CSDN博客

img danielli

Facebook Development and Authentation Mode

发表于2008/9/28 13:54:00  1111人阅读

分类: Web服务

 

使用 .NET Compact Framework 进行 Facebook 开发
1/4/2008

Peter Foot,In The Hand Ltd

2007 年 11 月

摘要

Facebook 已成为社会化网络领域备受人们关注的一个现象。该网站为开发人员提供了 API 以便支持开发 Web 应用程序和桌面应用程序。本文将探讨如何通过智能设备应用程序来利用此功能。本文随附的源代码演示了如何使用 Facebook API 的主要功能以及如何与特定于 Microsoft® Windows Mobile® 的 API 紧密集成。

您可以从 Microsoft 的共享源码托管站点 CodePlex http://www.codeplex.com/FacebookToolkit 上获得 Facebook 开发工具包(内含 Facebook.Compact 库和相关示例)。

适用于

Microsoft Visual Studio® 2005

Microsoft Windows Mobile 5.0

Microsoft Windows Mobile 6

简介

Facebook 开发工具包

支持 Compact Framework

注册 Facebook 应用程序

存储密钥

根证书

Facebook 朋友

处理活动

处理照片

总结

简介

Facebook 是一个最近非常流行的社会化网络网站。Facebook 提供了一个功能丰富的 API,通过它开发人员可以构建集成到 Facebook 网站的 Web 应用程序和可访问 Facebook 内容的独立桌面应用程序。本文将对后者进行探讨,并介绍如何通过 .NET Compact Framework 使用 Facebook 来构建独立的智能设备应用程序。

Facebook 开发工具包

Facebook 属于 Internet 服务,并提供了一个基于 Web 服务的 API。Facebook 使用具象状态传输 (REST) API,而不是很多托管代码开发人员熟悉的简单对象访问协议 (SOAP) Web 服务。由于无法使用 Visual Studio 自动创建与此服务进行交互的代码,因此您需要自己编写功能来发出请求和分析响应。幸运的是,Facebook 开发工具包已经帮助我们实现了这一点。这就是提供简单对象模型的桌面 .NET 库。此项目当时是为 Microsoft 创建的,作为初学者工具包向初学者展示 Visual Studio Express 的各种特性和功能;其当前发布版本并不支持 .NET Compact Framework。此项目由 Microsoft 的共享源码托管站点 CodePlex www.codeplex.com 托管。自从对如何让此工具包支持 Compact Framework 产生兴趣时起,我就加入了 CodePlex 项目,并开始进行修改。

 

我还必须额外编写一些代码,以便添加桌面版本正在使用的功能。这些代码主要用于支持 URL 编码字符串。特殊字符将在这里被替换,这样可以将该字符串作为请求 URL 的一部分或在窗体发布内部安全发送。还需要添加的代码是 WebClient.DownloadData 的一个基本实现,我是使用 HttpWebRequest 对其进行封装以便检索响应并返回原始字节的。

在编写桌面(或此示例中的智能设备)应用程序时,唯一无法只通过 API 即可实现的交互部分是初始身份验证。这必须通过传递应用程序 ID 并向用户显示熟悉的登录页面的网页才能实现。对于 .NET Compact Framework,可以通过承载 WebBrowser 控件的专用窗体来实现这一点。此方法为集成登录方法,因此我们不必在 Microsoft Internet Explorer® Mobile 中登录然后再切换回应用程序。不幸的是,由于登录屏幕没有特定于智能设备的版本,因此屏幕的外观不够理想。下面的图 1 显示了 Windows Mobile 设备上运行的登录屏幕。

图 1. Facebook API 登录页面

用户成功进行身份验证后,登录窗体将自动关闭。我们需要做的是检查控件所导航到的页面的 Url。对于不是 Web 托管的应用程序,此操作将重定向到 desktopapp.php,通知用户关闭窗口并返回到应用程序。桌面应用程序和智能设备应用程序使用相同的登录页面。代码位于 WebBrowser 控件上 Navigated 事件处理程序中:

private void wbFacebookLogin_Navigated(object sender,
      WebBrowserNavigatedEventArgs e)
{
      if (e.Url.PathAndQuery.IndexOf("desktopapp.php") > -1)
      {
         DialogResult = DialogResult.OK;
      }
}

我对代码所做的所有这些修改均未包含在 Facebook 开发工具包的当前安装程序包中,因此您需要下载源代码程序包。这其中不仅包含整个解决方案,还提供了库中的所有源代码。您可以从以下位置下载最新代码:

http://www.codeplex.com/FacebookToolkit/SourceControl/ListDownloadableCommits.aspx

将 .ZIP 文件解压缩到“我的文档”文件夹中的某个文件夹中。解决方案中有个名为 Device Samples 的文件夹,内含本文将引用的示例应用程序。

注册 Facebook 应用程序

您需要先注册并生成唯一的 API 密钥和密码,然后才能创建自己的 Facebook 应用程序。为此,首先需要将 Developer 应用程序添加到 Facebook 档案文件。添加此应用程序之后,转到该应用程序的页面 (http://www.facebook.com/developers/) 并单击图 2 显示的“Set Up New Application”(设置新应用程序)按钮。然后便可以为应用程序分配一个唯一名称。

图 2 Facebook 开发人员主页

下一页包含新应用程序所有配置选项中的字段。图 3 显示了如何填写示例应用程序的这些字段。您需要完成此过程以便获得可在以下示例中使用的 API 密钥。

图 3.“New Application”(新应用程序)页

创建应用程序之后,您可以从“My Applications”(我的应用程序)页查看应用程序 ID 和密码,此页可以从“Developer application”(开发人员应用程序)主页进行访问。图 4 显示了“My Applications”(我的应用程序)页。API 密钥和密码将显示在其中间位置的空白区域内。从应用程序登录 Facebook 时需要此密钥和密码。

图 4.“My Applications”(我的应用程序)页

现在您已具有通过应用程序与 Facebook 进行验证身份所需的所有信息。由于应用程序需要 API 密钥和密码,因此需要以某种方式将其安全地存储在设备中。

存储密钥

在应用程序中存储 API 密钥和密码值的方法很多。密钥和密码都是包含十六进制数据的字符串。桌面示例应用程序将这些值存储在配置文件中。为了简便,在这些示例应用程序中,我们将这些值写入设备上的注册表中。不应对已发布的产品执行此操作,因为任何人都可以通过注册表编辑器以纯文本方式查看。若要使用以下示例应用程序,需要将以下注册表值写入设备。可以使用 Visual Studio 随附的“远程注册表编辑器”工具。

HKEY_LOCAL_MACHINE/SOFTWARE/InTheHand/FacebookDeviceSample
ApplicationKey (String) = Your API Key
Secret (String) = Your Secret

以下所有示例应用程序都将从此注册表位置加载这些值。

根证书

为了安全起见,Facebook 身份验证使用 HTTPS 进行通信;但这会在 Windows Mobile 上会产生问题,因为默认情况下,设备并未提供 Facebook 使用的根证书。这意味着安全 HTTPS 请求将失败。幸运的是,可以通过下载根证书并将其添加到设备中来解决此问题。

有关根证书列表的信息,请访问 http://www.geotrust.com/resources/root_certificates/index.asp。由于列表太长,查找起来似乎很不方便,您可以查找条目“Root 5 - Equifax Secure Global eBusiness CA-1”。找到相关下载链接来保存 .CER 文件。提供的证书有 DER 编码和 Base-64 编码两种;如果使用的是 Windows Mobile 5.0,请确保选择此文件的 DER 编码版本。对于 Windows Mobile 6,则可以使用任一版本。

右键单击链接并将文件保存到计算机中,确保以扩展名 .CER 保存文件。将此文件复制到设备上,并使用设备上的“资源管理器”找到它,然后点击该文件进行安装。通过在部署中包含 .CER 文件并通过编程方式启动,即可自动安装该文件。

Facebook 朋友

现在您已具有执行示例应用程序的所有先决条件,接下来我们将深入探讨第一个示例应用程序。评价 Facebook 之类的社会化网站时首先要考虑的就是其维护联系人列表的能力。我们首先要看的示例是用于检索朋友列表的名为 DeviceFriendsSample 的示例。

我们已向应用程序窗体添加了一个 FacebookService 组件,这样便可提供与 Facebook API 进行的所有交互。首要任务就是将 API 密钥和密码分配给控件并与 Facebook 建立连接。

private void FacebookFriends_Load(object sender, EventArgs e)
{
      //连接到 Facebook
      facebookService1.ApplicationKey = 
            (string)Microsoft.Win32.Registry.GetValue(
       "HKEY_LOCAL_MACHINE//Software//InTheHand//FacebookDeviceSample", 
            "ApplicationKey", "");
      facebookService1.Secret = 
            (string)Microsoft.Win32.Registry.GetValue(

      "HKEY_LOCAL_MACHINE//Software//InTheHand//FacebookDeviceSample", 
            "Secret", "");

      facebookService1.ConnectToFacebook();

      RefreshFriendsList();
}

RefreshFriendsList 方法用于检索用户的朋友并将他们加载到 ListView 控件。为了减少屏幕更新,我们围绕代码对 ListView 控件调用 BeginUpdate 和 EndUpdate,以填充该列表。由于每个 User 都会被读取,因此我们用 Name 填充 ListViewItem,并将 PictureSmall 属性(一个 Image)分配给与 ListView 控件相关的 ImageList

private void RefreshFriendsList()
{
      lvFriends.Items.Clear();
      ilFriends.Images.Clear();

      Cursor.Current = Cursors.WaitCursor;
      lvFriends.BeginUpdate();

      Collection<User> friends = facebookService1.GetFriends();

      int i = 0;

      foreach (User u in friends)
      {
            ListViewItem lvi = new ListViewItem(u.Name);
            lvi.Tag = u;
            lvi.ImageIndex = i;
            ilFriends.Images.Add(u.PictureSmall);
            i++;
            lvFriends.Items.Add(lvi);
      }

      lvFriends.EndUpdate();
      Cursor.Current = Cursors.Default;
}

有用的网页

在桌面或 Web 应用程序上使用 Facebook API 时,很多标准 URL 可以连接到 Facebook 的特定页面。此过程需要活动网络连接,通过活动网络连接可以查看和执行无法通过 Facebook API 本身查看和执行的属性和操作。其中每个 URL 都有等效的移动 URL,移动 URL 的格式更适合设备使用。表 1 列出了所有移动格式 URL。

表 1. 公共 Facebook 页面 URL

URL 用途

http://m.facebook.com/profile.php?id=xxxxx

显示特定的用户档案文件。

http://m.facebook.com/event.php?eid=xxxxx

显示特定活动。

http://m.facebook.com/group.php?gid=xxxxx

显示特定群组。

http://m.facebook.com/poke.php?id=xxxxx

与特定用户交流。

http://m.facebook.com/message.php?id=xxxxx &subject=xxxxx&msg=xxxxx

向指定用户发送消息。

http://m.facebook.com/photo_search.php?id=xxxxx

查看特定用户的所有照片。

http://m.facebook.com/wall.php?id=xxxxx

阅读用户留言板。

http://m.facebook.com/notes.php?id=xxxxx

阅读用户发布的留言。

在上述“朋友”示例中,我们使用了其中的第一个 URL。第一个功能键上有一个“View”(查看)菜单项。点击此菜单项将打开指向特定用户的“Internet Explorer Mobile”窗口。还可以使用应用程序中承载的 WebBrowser 控件来提高集成度。这里存在一个限制,即使用网站需要单独登录,但是用户可以保存登录信息,这样在长时间不用时用户可收到提示信息。启动特定 URL 需要单独调用 Process.Start

System.Diagnostics.Process.Start("http://m.facebook.com/profile.php?id=" 
      + u.UserId, "");

图 5 显示了 Internet Explorer Mobile 中的联系人档案文件。

图 5.“Profile”(档案文件)页面

同步

正如我们已经看到的那样,为了保护隐私,未通过 API 公开用户的某些重要信息。不过,如果我们能够让用户图片与 Outlook Mobile 同步就非常有用了,这样用户就可以在来电屏幕和消息窗体中使用这些图片。使用 Microsoft.WindowsMobile.PocketOutlook 命名空间中的托管 Pocket Outlook 类可以完成同步。在 DeviceFriendsSample 应用程序的菜单上有一个“Sync”(同步)项。点击该项将启动朋友检索功能,并在用户通讯簿中检查匹配的联系人。当找到姓名匹配的联系人时,就会开启此联系人项进行编辑。如果没有找到匹配的联系人,我们将创建一个新的“Contact”(联系人)项。为了给某个“Contact”(联系人)指定一张图片,必须首先将该图片保存到某个临时文件中,该文件夹可以在指定图片后予以删除。Microsoft Office Outlook® Mobile 可以在内部存储图片,所以我们不必占用显式存储联系人图片文件的空间(同步过程期间除外)。显然,下载大量图片会占用大量网络带宽,所以当设备连接的网络是付费网络时,请酌情考虑。

下列代码段显示的是同步代码:

private void mnuSync_Click(object sender, EventArgs e)
{
   foreach (ListViewItem lvi in lvFriends.Items)
   {
      Contact c = null;
      User u = (User)lvi.Tag;

      //查找 poom 中的用户
      ContactCollection matches = 
         session.Contacts.Items.Restrict("[FirstName] = /"" + u.FirstName 
         + "/" AND [LastName] = /"" + u.LastName + "/"");
                
      if (matches != null && matches.Count > 0)
      {
         c = matches[0];
      }
      else
      {
         //添加新的联系人
         c = session.Contacts.Items.AddNew();
         c.FirstName = u.FirstName;
         c.LastName = u.LastName;

         //将其标记为 facebook 联系人
         c.Categories = "Facebook";
      }

      //创建临时文件名
      string filename = 
         System.IO.Path.Combine(System.IO.Path.GetTempPath(), 
         u.UserId + ".jpg");

      //如果已存在,将其删除
      if (System.IO.File.Exists(filename))
      {
         System.IO.File.Delete(filename);
      }

      //保存和指定图片
      u.PictureBig.Save(filename, 
         System.Drawing.Imaging.ImageFormat.Jpeg);
      c.SetPicture(filename);

      //删除临时文件(Outlook 存储了一个副本)
      System.IO.File.Delete(filename);

      //存储将来要引用的用户 ID
      c.Properties.Add("Facebook.UserId", typeof(string));
      c.Properties["Facebook.UserId"] = u.UserId;

      //保存此联系人
      c.Update();

   }
}

处理活动

Facebook 支持用户创建任何用途的日历活动。请勿将其与 .NET 事件混淆,这些活动更类似于 Outlook Mobile 中的日历约会。您可以邀请朋友,也可以创建任何人都可以注册参加的公共活动。通过 Facebook API,您可以检索所有邀请您参加的活动(或已创建活动)的详细信息。您也可以查看成员当前的出席情况(参加、不参加等等)。不能通过 Facebook API 创建或修改活动。下一个示例应用程序 DeviceEventSample 将展示如何检索活动的详细信息。我们展示了从 Facebook 返回的位于 ListView 控件中的活动列表(其中包括活动的图片)。图 6 对该屏幕进行了说明。由于活动图片的长宽比不固定,因此本示例中的图片出现了某种程度的拉伸。解决此问题的办法是使用其他控制进行显示(例如,所有者描述的列表控件),或将图片复制到已预调大小的画布,从而确保所有图片比例一致。由于本示例旨在向您展示 Facebook API,因此我们将其作为读者改善用户界面的一个练习。

图 6. Facebook 活动

我们可以看到,使用 FacebookEvent 项目集合非常简单。通过使用此信息您可以实现的很多有趣功能之一就是使用 Outlook Mobile 将活动副本存储到您的“日历”中。这使您能够使用提醒功能的所有常规功能,并会在“今日”主页屏幕上显示即将到来的活动。我已向窗体添加了一个 ContextMenu ,并让其与 ListView 控件上的 ContextMenu 属性相关联。点击“ContextMenu”时,我们首先要检查项目当前是否已选定,然后运行 AddToCalendar 方法:

private void AddToCalendar()
{
      foreach (int i in lvEvents.SelectedIndices)
      {
            if (i > -1)
            {
                  Appointment eventAppointment = null;
                  FacebookEvent fe = (FacebookEvent)lvEvents.Items[i].Tag;
                  Collection<EventUser> attendees = 
                        facebookService1.GetEventMembers(fe.EventId);

                  //如果已存在,请选中 - 若如此,则需要对其进行更新
                  AppointmentCollection appointments = 
                        os.Appointments.Items.Restrict("[Categories] = /"Facebook/"");

                  foreach (Appointment a in appointments)
                  {
                        if ((string)a.Properties["Facebook.EventId"] == fe.EventId)
                        {
                              eventAppointment = a;
                              break;
                        }
                  }

                  //如果未找到,请添加一个新项……
                  if(eventAppointment == null)
                  {
                        eventAppointment = os.Appointments.Items.AddNew();
                        eventAppointment.Categories = "Facebook";
                        //在自定义属性中添加活动 ID
                    eventAppointment.Properties.Add("Facebook.EventId", 
                            typeof(string));
                        eventAppointment.Properties["Facebook.EventId"] = fe.EventId;                        
                  }
                    
                  eventAppointment.Subject = fe.Name;
                  eventAppointment.Location = fe.Location;
                  eventAppointment.Body = fe.Description;
                  eventAppointment.Start = fe.StartDate;
                  eventAppointment.End = fe.EndDate;
                  RecipientCollection rc = eventAppointment.Recipients;
                  //清除当前的列表
                  for (int irecipient = rc.Count-1; irecipient > -1; irecipient--)
                  {
                        rc.Remove(irecipient);
                  }
                  foreach (EventUser eu in attendees)
                  {
                        if (eu.Attending == RSVPStatus.Attending)
                        {
                              //添加收件人项(无有效电子邮件地址)
                              Recipient r = rc.Add(new Recipient(eu.User.Name, 
                                    "facebook"));
                        }
                  }
                  eventAppointment.Update();
            } 
      }
}

在设置“Appointment”(约会)的几个标准属性时,有几个项需要进一步说明一下。首先,您应注意到我们已向所有以编程方式创建的项目指定了“Facebook”类。这使得在属于 Facebook 项目的日历中搜索现有项目时更加容易。其次,我们充分利用 Windows Mobile 5.0 的 iPocket Outlook API 中的一个功能,以及其他我们可以籍此向项目添加自定义属性的功能。我们可以使用这一机制将 Facebook 中的 eventid 与该项关联起来。我将此属性称为“Facebook.EventId”以使其具有唯一性,因为“EventId”很容易让人想到用于其他目的。最后,我们利用了 Outlook Mobile 对会议项目的支持功能。我们把所有已确认参加此次活动的与会者姓名填充会议收件人集合。这只是为了便于显示,而不能像传统 Outlook 收件人对于会议功能(您可以更新会议功能并通知收件人)那样来使用,因为 Facebook API 并未公开电子邮件地址。仅是在现有“Appointment”(约会)对话框中存储与会者的一种便利方法。创建活动后,该活动将与桌面机上的 Outlook 同步,用户就可以使用这一项来做自己想做的任何事情,例如通过红外或 Bluetooth 进行收发,或者设置铃声提醒。图 7 显示了在“Calendar”(日历)应用程序中已创建的约会项。

图 7.“Calendar”(日历)中的 Facebook 活动

处理照片

Facebook 允许其用户创建多个照片相册,每个相册最多可以上传 60 张照片。还允许您向图片中添加标记,以便显示图片中包含了哪些朋友的照片。有些操作无法通过 Facebook API 来执行,例如,您不能删除或修改相册或照片。添加新照片时,其他用户看不到该照片。必须先通过 Facebook 网站激活照片,照片才能在您的相册中正常显示。本节的示例应用程序是一个用于浏览和管理自己照片相册的工具。

此应用程序的主窗体使用 ListView 控件来显示每个相册的缩略图。图 8 是一个已经打开且包含几个相册的窗体。

图 8. Facebook 照片

在用户选中某特定相册时,将打开一个显示该相册中所有照片的新窗体。通过主窗体,您还可以添加新的相册并查看当前所选相册的属性 - 名称、位置和说明。点击“New Album”(新建相册)将打开一个新的 AlbumPropertiesForm 实例,您可以通过该实例输入相册信息。点击“确定”关闭该窗体时,将调用 API 创建一个新相册:

private void AlbumPropertiesForm_Closing(object sender, CancelEventArgs e)
{
      if (isnew)
      {
            if(!string.IsNullOrEmpty(txtName.Text))
            {
                  Album newAlbum = parent.facebookService1.CreateAlbum(
                        txtName.Text, txtLocation.Text, txtDescription.Text);
            }
      }
}

当用户返回到主窗体时,将刷新列出的相册。相册创建之后会立即出现在用户的相册集中,即便该相册中没有照片。

当选中某个相册时,我们打开一个新的 AlbumForm 实例,用来传递相册详细信息。该 AlbumForm 设计方式与主窗体类似,为简化起见,我们使用内置的 ListView 控件。照片通过窗体的 Load 事件处理程序进行上传

private void AlbumForm_Load(object sender, EventArgs e)
{
      Cursor.Current = Cursors.WaitCursor;
      lvAlbum.BeginUpdate();

      Collection<Photo> photos =
            parentForm.facebookService1.GetPhotos(album.AlbumId);
      int i = 0;
      foreach (Photo p in photos)
      {
            ListViewItem lvi = new ListViewItem(p.Caption);
            lvi.ImageIndex = i;
            ilAlbum.Images.Add(p.PictureSmall);
            lvAlbum.Items.Add(lvi);
            i++;
      }
      lvAlbum.EndUpdate();
      Cursor.Current = Cursors.Default;
}

由于图片宽长比不同,所以此处使用 ListView 和 ImageList 进行显示可能会导致某些图片失真。然而,它提供了一个快速再现相册内容的方法。图 9 显示了典型相册的外观。

图 9. “Album”(相册)窗体

您可以通过此窗体的一个菜单项将新图片上传到相册。与创建新相册不同的是,在通过 Facebook 网站授权之前,新上传的照片将不可见。当您在网站上查看该相册时,新上传的照片将显示为待审批的待定项列表。无法通过网站的移动版审批照片。为了集中采集图片,我们使用了 Microsoft.WindowsMobile.Forms 程序集的 CameraCaptureDialog。

private void mnuPhoto_Click(object sender, EventArgs e)
{
      Microsoft.WindowsMobile.Forms.CameraCaptureDialog ccd = 
            new Microsoft.WindowsMobile.Forms.CameraCaptureDialog();
      ccd.Mode = Microsoft.WindowsMobile.Forms.CameraCaptureMode.Still;
      ccd.Title = "Add photo to Facebook";
      ccd.Resolution = new Size(640, 480);

      //如果成功
      if (ccd.ShowDialog() == DialogResult.OK)
      {
            System.IO.FileInfo fiImage = new System.IO.FileInfo(ccd.FileName);
            parentForm.facebookService1.UploadPhoto(album.AlbumId, fiImage);
      }
}

UploadPhoto 方法采用一个 albumid 和一个包含图像文件详细信息的 FileInfo。FileInfo 构造函数会传递相机对话框中的完整文件路径。

总结

从本文探讨的示例中我们了解了如何使用 Facebook 开发人员工具包提供的功能。尽管 Facebook 不能通过其 API 提供站点的所有功能,但是 Facebook 开发人员工具包提供了很多用于检索朋友、群组、活动和照片信息的功能。接下来了解了如何利用 Windows Mobile API 将 Facebook 数据与设备联系人和日历应用程序相集成。最后介绍了如何直接从移动设备浏览照片,并将其上传到在线相册中。

另请参阅

Facebook 开发最近引起了不小的轰动。其中很有趣的一个例子就是称为 OutSync 的桌面应用程序。此程序在桌面机上与 Outlook 一起运行,可以将 Facebook 档案文件图片与您的联系人进行同步,而 Outlook 随后会让联系人与 Windows Mobile 设备同步。其原理与我们上面创建的代码类似,但此程序具有丰富的 Windows Presentation Foundation (WPF) 用户界面。频道 10 提供了一个演示本产品的视频:http://www.on10.net/Blogs/laura/sync-your-facebook-contacts-with-outlook-and-windows-mobile/

作者简介

Peter Foot 是 In The Hand Limited 的创始人,这是一家为移动设备提供软件开发和咨询服务的公司。In The Hand 还针对 .NET Compact Framework 开发出了屡获殊荣的软件组件来协助其他开发人员进行开发。2007 年,Peter 还与他人合著了《Microsoft Mobile Development Handbook》,该书由 Microsoft Press® 出版。

Peter 创建的 32feet.NET 共享源码社区项目为 .NET 开发人员使用 Bluetooth 和 IrDA 技术提供了很多便利。Peter 还积极参与其他多项共享源码计划。

在参与 Windows Mobile 之前,Peter 就职于一家英国移动门户网站,带领其团队从事英国首个客户 GPRS 服务方面的测试工作。Peter 拥有计算机科学理学学士学位。

0 0

相关博文

我的热门文章

img
取 消
img