纳金网

标题: 如何通过Unity iOS应用程序在Twitter上分享图片和链接 [打印本页]

作者: 驰骋的风    时间: 2018-11-1 14:54
标题: 如何通过Unity iOS应用程序在Twitter上分享图片和链接
Unity不仅是最受欢迎和强大的游戏引擎,它具有用于开发iOS和游戏的优秀视觉编辑器,同时也具有很多功能,功能和社区的问题处理以及良好的定价选项。 毫无疑问,Unity被认为是最好的构建iOS和Android游戏和应用程序的引擎。

今天在本教程中,我正在分享一个与iOS应用程序相关的主题,帮助您轻松地从应用程序分享图片或链接到twitter。 而分享图像不是一项重大任务,而是包含iOS,用户必须首先上传图像或先在设备路径上写入图像,然后从该路径读取图像,因为iOS中不存在资源文件夹。 所以借助下面的代码,您可以轻松地将图像读取和写入设备路径。

让我们看看如何实现:
首先在Twitter上创建一个应用程序,并保存应用程序的消费者密钥,秘密密钥,媒体URL和Tweet URL。
那么为了在Twitter上分享,用户必须在Twitter上登录。 所以让我们使用下面提到的代码登录用户。

现在,我们创建三个UI按钮。
1.Twitter登录按钮//点击TwitterLogin
2.提交Pin Button //点击SubmitPin()
3.Twitter分享按钮//在点击中调用Twitter()。
并创建一个InputField来输入您在调用TwittterLogin()函数后收到的PIN码。

1. InputFieldPin

启动Twitter登录按钮。 然后成功禁用登录按钮并启用输入框并提交按钮。 然后在成功登录后,启用“共享”按钮并禁用所有其他按钮

现在创建两个脚本:
TwitterHandller.cs——
  1. <font color="#000000" face="Arial">using UnityEngine;
  2. using UnityEngine.UI;
  3. using System.Collections;
  4. using System;
  5. using System.IO;
  6. using System.Net;
  7. using System.Xml;
  8. using System.Collections.Generic;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11. using System.Globalization;
  12. using System.Linq;
  13. using System.Security.Cryptography;
  14. using UnityEngine;
  15. using SimpleJSON;
  16. using System.Runtime.InteropServices;
  17. public class TwitterHandller: MonoBehaviour
  18. {
  19.      Twitter.RequestTokenResponse m_RequestTokenResponse;
  20.   
  21.     Twitter.AccessTokenResponse m_AccessTokenResponse;
  22.     public string CONSUMER_KEY;//Put oun consumer key here
  23.   
  24.      public string CONSUMER_SECRET;//Put own consumer secret here
  25.     // Use this for initialization
  26.      
  27.     void Start()
  28.     {
  29.             LoadUserTwitterInfo();
  30.      }
  31.   void LoadUserTwitterInfo()
  32.     {
  33.            m_AccessTokenResponse = new Twitter.AccessTokenResponse();                    
  34.            m_AccessTokenResponse.UserId =          PlayerPrefs.GetString(PLAYER_PREFS_TWITTER_USER_ID);
  35.   
  36.            m_AccessTokenResponse.ScreenName =  PlayerPrefs.GetStrin(PLAYER_PREFS_TWITTER_USER_SCREEN_NAME);
  37.   
  38.            m_AccessTokenResponse.Token = PlayerPrefs.GetString(PLAYER_PREFS_TWITTER_USER_TOKEN);
  39.            
  40.            m_AccessTokenResponse.TokenSecret = PlayerPrefs.GetString(PLAYER_PREFS_TWITTER_USER_TOKEN_SECRET);
  41.             //Debug.Log ("ID"+m_AccessTokenResponse.UserId );
  42.          
  43.            // Debug.Log ("Name"+m_AccessTokenResponse.ScreenName );
  44.      
  45.           // Debug.Log ("Token"+m_AccessTokenResponse.Token );
  46.      
  47.           // Debug.Log ("Token Secret"+m_AccessTokenResponse.TokenSecret );
  48.   
  49.            if (!string.IsNullOrEmpty(m_AccessTokenResponse.Token) &&
  50.           !string.IsNullOrEmpty                (m_AccessTokenResponse.ScreenName) &&
  51.          !string.IsNullOrEmpty(m_AccessTokenResponse.Token) &&
  52.          !string.IsNullOrEmpty(m_AccessTokenResponse.TokenSecret))
  53.           {
  54.                 string log = "LoadTwitterUserInfo - succeeded";
  55.   
  56.                  log += "\n    UserId : " + m_AccessTokenResponse.UserId;
  57.   
  58.                 log += "\n    ScreenName : " + m_AccessTokenResponse.ScreenName;
  59.      
  60.                  log += "\n    Token : " + m_AccessTokenResponse.Token;
  61.       
  62.                  log += "\n    TokenSecret : " + m_AccessTokenResponse.TokenSecret;
  63.      
  64.                  print(log);
  65.              }
  66.   
  67.       }
  68. public void TwitterLogin()
  69. {
  70.     //Calling GetRequestToken function of Twitter API use your own consumer key and consumer secret.
  71.      StartCoroutine(Twitter.API.GetRequestToken(CONSUMER_KEY, CONSUMER_SECRET,
  72.                                                            new  Twitter.RequestTokenCallback(this.OnRequestTokenCallback)));
  73. }
  74. public void  SubmitPin()
  75. {
  76.    StartCoroutine(Twitter.API.GetAccessToken(CONSUMER_KEY, CONSUMER_SECRET, m_RequestTokenResponse.Token, m_PIN,
  77.                            new Twitter.AccessTokenCallback(this.OnAccessTokenCallback)));
  78. }
  79. public void TwitterShare()
  80. {
  81. //put url of the image that you want to share
  82.          string url="https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/8e/d7/e9         /8ed7e940-9fda-d1ca-995e-d4e2086a438c/pr_source.jpg/150x150bb.jpg";
  83.         string savePath = Path.Combine(Application.persistentDataPath, "data");
  84. //downloading image from url and saving to path
  85.         downloadImage(url, savePath);
  86. //loading image from the path in byte array
  87.         byte[] imageBytes = loadImage(savePath);
  88. //Calling PostTweet  function of  Twitter   API
  89. StartCoroutine(Twitter.API.PostTweet(imageBytes,m_Tweet,CONSUMER_KEY,CONSUMER_SECRET,     m_AccessTokenResponse,  new Twitter.PostTweetCallback(this.OnPostTweet)));
  90. }
  91. }</font>
复制代码
Twitter.cs——
  1. <font color="#000000" face="Arial">using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Xml;
  5. using System.Collections.Generic;
  6. using System.Text;
  7. using System.Collections;
  8. using System.Text.RegularExpressions;
  9. using System.Globalization;
  10. using System.Linq;
  11. using System.Security.Cryptography;
  12. using UnityEngine;
  13. using SimpleJSON;
  14. using System.IO;
  15. using System.Runtime.InteropServices;
  16. namespace Twitter
  17. {
  18.      
  19.    
  20.     public class RequestTokenResponse
  21.     {
  22.         public string Token { get; set; }
  23.         public string TokenSecret { get; set; }
  24.     }
  25.     public class AccessTokenResponse
  26.     {
  27.         public string Token { get; set; }
  28.         public string TokenSecret { get; set; }
  29.         public string UserId { get; set; }
  30.         public string ScreenName { get; set; }
  31.     }
  32.     public delegate void RequestTokenCallback(bool success, RequestTokenResponse response);
  33.     public delegate void AccessTokenCallback(bool success, AccessTokenResponse response);
  34.     public delegate void PostTweetCallback(bool success);
  35.     public delegate void GetTimelineCallback(bool success);
  36.   
  37.     public class API
  38.     {
  39.         #region OAuth Token Methods
  40.         // 1. Get Request-Token From Twitter
  41.         // 2. Get PIN from User
  42.         // 3. Get Access-Token from Twitter
  43.         // 4. Use Accss-Token for APIs requriring OAuth
  44.         // Accss-Token will be always valid until the user revokes the access to your application.
  45.         // Twitter APIs for OAuth process
  46.         private static readonly string RequestTokenURL = "https://api.twitter.com/oauth/request_token";
  47.         private static readonly string AuthorizationURL = "https://api.twitter.com/oauth/authenticate?oauth_token={0}";
  48.         private static readonly string AccessTokenURL = "https://api.twitter.com/oauth/access_token";
  49.         public static IEnumerator GetRequestToken(string consumerKey, string consumerSecret, RequestTokenCallback callback)
  50.         {
  51.             WWW web = WWWRequestToken(consumerKey, consumerSecret);
  52.             yield return web;
  53.             if (!string.IsNullOrEmpty(web.error))
  54.             {
  55.                 Debug.Log(string.Format("GetRequestToken - failed. error : {0}", web.error));
  56.                 callback(false, null);
  57.             }
  58.             else
  59.             {
  60.                 RequestTokenResponse response = new RequestTokenResponse
  61.                 {
  62.                     Token = Regex.Match(web.text, @"oauth_token=([^&]+)").Groups[1].Value,
  63.                     TokenSecret = Regex.Match(web.text, @"oauth_token_secret=([^&]+)").Groups[1].Value,
  64.                 };
  65.                 if (!string.IsNullOrEmpty(response.Token) &&
  66.                     !string.IsNullOrEmpty(response.TokenSecret))
  67.                 {
  68.                     callback(true, response);
  69.                 }
  70.                 else
  71.                 {
  72.                     Debug.Log(string.Format("GetRequestToken - failed. response : {0}", web.text));
  73.                     callback(false, null);
  74.                 }
  75.             }
  76.         }
  77.         public static void OpenAuthorizationPage(string requestToken)
  78.         {
  79.             Application.OpenURL(string.Format(AuthorizationURL, requestToken));
  80.         }
  81.         public static IEnumerator GetAccessToken(string consumerKey, string consumerSecret, string requestToken, string pin, AccessTokenCallback callback)
  82.         {
  83.             Debug.Log ("GetAccessToken");
  84.             WWW web = WWWAccessToken(consumerKey, consumerSecret, requestToken, pin);
  85.             yield return web;
  86.             if (!string.IsNullOrEmpty(web.error))
  87.             {
  88.                 Debug.Log(string.Format("GetAccessToken - failed. error : {0}", web.error));
  89.                 callback(false, null);
  90.             }
  91.             else
  92.             {
  93.                 AccessTokenResponse response = new AccessTokenResponse
  94.                 {
  95.                     Token = Regex.Match(web.text, @"oauth_token=([^&]+)").Groups[1].Value,
  96.                     TokenSecret = Regex.Match(web.text, @"oauth_token_secret=([^&]+)").Groups[1].Value,
  97.                     UserId = Regex.Match(web.text, @"user_id=([^&]+)").Groups[1].Value,
  98.                     ScreenName = Regex.Match(web.text, @"screen_name=([^&]+)").Groups[1].Value
  99.                 };
  100.                 if (!string.IsNullOrEmpty(response.Token) &&
  101.                     !string.IsNullOrEmpty(response.TokenSecret) &&
  102.                     !string.IsNullOrEmpty(response.UserId) &&
  103.                     !string.IsNullOrEmpty(response.ScreenName))
  104.                 {
  105.                     callback(true, response);
  106.                 }
  107.                 else
  108.                 {
  109.                     Debug.Log(string.Format("GetAccessToken - failed. response : {0}", web.text));
  110.                     callback(false, null);
  111.                 }
  112.             }
  113.         }
  114.         private static WWW WWWRequestToken(string consumerKey, string consumerSecret)
  115.         {
  116.             // Add data to the form to post.
  117.             WWWForm form = new WWWForm();
  118.             form.AddField("oauth_callback", "oob");
  119.             // HTTP header
  120.             Dictionary<string, string> parameters = new Dictionary<string, string>();
  121.             AddDefaultOAuthParams(parameters, consumerKey, consumerSecret);
  122.             parameters.Add("oauth_callback", "oob");
  123.             Dictionary<string, string> headers = new Dictionary<string, string>();
  124.             headers["Authorization"] = GetFinalOAuthHeader("POST", RequestTokenURL, parameters);
  125.             return new WWW(RequestTokenURL, form.data, headers);
  126.         }
  127.         private static WWW WWWAccessToken(string consumerKey, string consumerSecret, string requestToken, string pin)
  128.         {
  129.             // Need to fill body since Unity doesn't like an empty request body.
  130.             byte[] dummmy = new byte[1];
  131.             dummmy[0] = 0;
  132.             // HTTP header
  133.             Dictionary<string, string> headers = new Dictionary<string, string>();
  134.             Dictionary<string, string> parameters = new Dictionary<string, string>();
  135.             AddDefaultOAuthParams(parameters, consumerKey, consumerSecret);
  136.             parameters.Add("oauth_token", requestToken);
  137.             parameters.Add("oauth_verifier", pin);
  138.             headers["Authorization"] = GetFinalOAuthHeader("POST", AccessTokenURL, parameters);
  139.             return new WWW(AccessTokenURL, dummmy, headers);
  140.         }
  141.         private static string GetHeaderWithAccessToken(string httpRequestType, string apiURL, string consumerKey, string consumerSecret, AccessTokenResponse response, Dictionary<string, string> parameters)
  142.         {
  143.             AddDefaultOAuthParams(parameters, consumerKey, consumerSecret);
  144.             parameters.Add("oauth_token", response.Token);
  145.             parameters.Add("oauth_token_secret", response.TokenSecret);
  146.             return GetFinalOAuthHeader(httpRequestType, apiURL, parameters);
  147.         }
  148.         #endregion
  149. #region Twitter API Methods
  150.         
  151.         private const string PostTweetURL = "https://api.twitter.com/1.1/statuses/update.json";
  152.   private const string GetTimelineURL = "https://api.twitter.com/1.1/statuses/home_timeline.json";
  153. private const string UploadMediaURL = "https://upload.twitter.com/1.1/media/upload.json";
  154. public static IEnumerator PostTweet(byte[] imageBytes, string text, string consumerKey, string consumerSecret, AccessTokenResponse response, PostTweetCallback callback)
  155.         {
  156.             Dictionary<string, string> mediaParameters = new Dictionary<string, string>();
  157.             string encoded64ImageData = Convert.ToBase64String(imageBytes);
  158.             mediaParameters.Add("media_data", encoded64ImageData);
  159.             // Add data to the form to post.
  160.             WWWForm mediaForm = new WWWForm();
  161.             mediaForm.AddField("media_data", encoded64ImageData);
  162.             // HTTP header
  163.             Dictionary<string, string> mediaHeaders = new Dictionary<string, string>();
  164.             string auth = GetHeaderWithAccessToken("POST", UploadMediaURL, consumerKey, consumerSecret, response, mediaParameters);
  165.             mediaHeaders.Add("Authorization", auth);
  166.             mediaHeaders.Add("Content-Transfer-Encoding", "base64");
  167.             WWW mw = new WWW(UploadMediaURL, mediaForm.data, mediaHeaders);
  168.             yield return mw;
  169.             string mID = Regex.Match(mw.text, @"(\Dmedia_id\D\W)(\d*)").Groups[2].Value;
  170.             Debug.Log("response from media request : " + mw.text);
  171.             Debug.Log("mID = " + mID);
  172.             if (!string.IsNullOrEmpty(mw.error))
  173.             {
  174.                 Debug.Log(string.Format("PostTweet - failed. {0}\n{1}", mw.error, mw.text));
  175.                 callback(false);
  176.             }
  177.             else
  178.             {
  179.                 string error = Regex.Match(mw.text, @"<error>([^&]+)</error>").Groups[1].Value;
  180.                 if (!string.IsNullOrEmpty(error))
  181.                 {
  182.                     Debug.Log(string.Format("PostTweet - failed. {0}", error));
  183.                     callback(false);
  184.                 }
  185.                 else
  186.                 {
  187.                     callback(true);
  188.                 }
  189.             }
  190.             string url="https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/8e/d7/e9/8ed7e940-9fda-d1ca-995e-d4e2086a438c/pr_source.jpg/150x150bb.jpg";
  191.             Dictionary<string, string> parameters = new Dictionary<string, string>();
  192.             parameters.Add("status", url);
  193.             parameters.Add("media_ids", mID);
  194.             //parameters.Add("url", url);
  195.             // Add data to the form to post.
  196.             WWWForm form = new WWWForm();
  197.             form.AddField("status", url);
  198.             form.AddField("media_ids", mID);
  199.             //form.AddField("url", url);
  200.             // HTTP header
  201.             var headers = new Dictionary<string, string>();
  202.             headers["Authorization"] = GetHeaderWithAccessToken("POST", PostTweetURL, consumerKey, consumerSecret, response, parameters);
  203.             WWW web = new WWW(PostTweetURL, form.data, headers);
  204.             yield return web;
  205.             if (!string.IsNullOrEmpty(web.error))
  206.             {
  207.                 Debug.Log(string.Format("PostTweet - failed. {0}\n{1}", web.error, web.text));
  208.                 callback(false);
  209.             }
  210.             else
  211.             {
  212.                 string error = Regex.Match(web.text, @"<error>([^&]+)</error>").Groups[1].Value;
  213.                 if (!string.IsNullOrEmpty(error))
  214.                 {
  215.                     Debug.Log(string.Format("PostTweet - failed. {0}", error));
  216.                     callback(false);
  217.                 }
  218.                 else
  219.                 {
  220.                     callback(true);
  221.                 }
  222.             }
  223.         }
  224. #endregion
  225. #region OAuth Help Methods
  226.   private static readonly string[] OAuthParametersToIncludeInHeader = new[]
  227.                                                           {
  228.                                                               "oauth_version",
  229.                                                               "oauth_nonce",
  230.                                                               "oauth_timestamp",
  231.                                                               "oauth_signature_method",
  232.                                                               "oauth_consumer_key",
  233.                                                               "oauth_token",
  234.                                                               "oauth_verifier"
  235.                                                               // Leave signature omitted from the list, it is added manually
  236.                                                               // "oauth_signature",
  237.                                                           };
  238.         private static readonly string[] SecretParameters = new[]
  239.                                                                 {
  240.                                                                     "oauth_consumer_secret",
  241.                                                                     "oauth_token_secret",
  242.                                                                     "oauth_signature"
  243.                                                                 };
  244.         private static void AddDefaultOAuthParams(Dictionary<string, string> parameters, string consumerKey, string consumerSecret)
  245.         {
  246.             parameters.Add("oauth_version", "1.0");
  247.             parameters.Add("oauth_nonce", GenerateNonce());
  248.             parameters.Add("oauth_timestamp", GenerateTimeStamp());
  249.             parameters.Add("oauth_signature_method", "HMAC-SHA1");
  250.             parameters.Add("oauth_consumer_key", consumerKey);
  251.             parameters.Add("oauth_consumer_secret", consumerSecret);
  252.         }
  253.         private static string GetFinalOAuthHeader(string HTTPRequestType, string URL, Dictionary<string, string> parameters)
  254.         {
  255.             // Add the signature to the oauth parameters
  256.             string signature = GenerateSignature(HTTPRequestType, URL, parameters);
  257.             parameters.Add("oauth_signature", signature);
  258.             StringBuilder authHeaderBuilder = new StringBuilder();
  259.             authHeaderBuilder.AppendFormat("OAuth realm=\"{0}\"", "Twitter API");
  260.             var sortedParameters = from p in parameters
  261.                                    where OAuthParametersToIncludeInHeader.Contains(p.Key)
  262.                                    orderby p.Key, UrlEncode(p.Value)
  263.                                    select p;
  264.             foreach (var item in sortedParameters)
  265.             {
  266.                 authHeaderBuilder.AppendFormat(",{0}=\"{1}\"", UrlEncode(item.Key), UrlEncode(item.Value));
  267.             }
  268.             authHeaderBuilder.AppendFormat(",oauth_signature=\"{0}\"", UrlEncode(parameters["oauth_signature"]));
  269.             return authHeaderBuilder.ToString();
  270.         }
  271.         private static string GenerateSignature(string httpMethod, string url, Dictionary<string, string> parameters)
  272.         {
  273.             var nonSecretParameters = (from p in parameters
  274.                                        where !SecretParameters.Contains(p.Key)
  275.                                        select p);
  276.             // Create the base string. This is the string that will be hashed for the signature.
  277.             string signatureBaseString = string.Format(CultureInfo.InvariantCulture,
  278.                                                        "{0}&{1}&{2}",
  279.                                                        httpMethod,
  280.                                                        UrlEncode(NormalizeUrl(new Uri(url))),
  281.                                                        UrlEncode(nonSecretParameters));
  282.             // Create our hash key (you might say this is a password)
  283.             string key = string.Format(CultureInfo.InvariantCulture,
  284.                                        "{0}&{1}",
  285.                                        UrlEncode(parameters["oauth_consumer_secret"]),
  286.                                        parameters.ContainsKey("oauth_token_secret") ? UrlEncode(parameters["oauth_token_secret"]) : string.Empty);
  287.             // Generate the hash
  288.             HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.ASCII.GetBytes(key));
  289.             byte[] signatureBytes = hmacsha1.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString));
  290.             return Convert.ToBase64String(signatureBytes);
  291.         }
  292.         private static string GenerateTimeStamp()
  293.         {
  294.             // Default implementation of UNIX time of the current UTC time
  295.             TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
  296.             return Convert.ToInt64(ts.TotalSeconds, CultureInfo.CurrentCulture).ToString(CultureInfo.CurrentCulture);
  297.         }
  298.         private static string GenerateNonce()
  299.         {
  300.             // Just a simple implementation of a random number between 123400 and 9999999
  301.             return new System.Random().Next(123400, int.MaxValue).ToString("X", CultureInfo.InvariantCulture);
  302.         }
  303.         private static string NormalizeUrl(Uri url)
  304.         {
  305.             string normalizedUrl = string.Format(CultureInfo.InvariantCulture, "{0}://{1}", url.Scheme, url.Host);
  306.             if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
  307.             {
  308.                 normalizedUrl += ":" + url.Port;
  309.             }
  310.             normalizedUrl += url.AbsolutePath;
  311.             return normalizedUrl;
  312.         }
  313.         private static string UrlEncode(string value)
  314.         {
  315.             if (string.IsNullOrEmpty(value))
  316.             {
  317.                 return string.Empty;
  318.             }
  319.             value = Uri.EscapeDataString(value);
  320.             // UrlEncode escapes with lowercase characters (e.g. %2f) but oAuth needs %2F
  321.             value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());
  322.             // these characters are not escaped by UrlEncode() but needed to be escaped
  323.             value = value
  324.                 .Replace("(", "%28")
  325.                 .Replace(")", "%29")
  326.                 .Replace("$", "%24")
  327.                 .Replace("!", "%21")
  328.                 .Replace("*", "%2A")
  329.                 .Replace("'", "%27");
  330.             // these characters are escaped by UrlEncode() but will fail if unescaped!
  331.             value = value.Replace("%7E", "~");
  332.             return value;
  333.         }
  334.         private static string UrlEncode(IEnumerable<KeyValuePair<string, string>> parameters)
  335.         {
  336.             StringBuilder parameterString = new StringBuilder();
  337.             var paramsSorted = from p in parameters
  338.                                orderby p.Key, p.Value
  339.                                select p;
  340.             foreach (var item in paramsSorted)
  341.             {
  342.                 if (parameterString.Length > 0)
  343.                 {
  344.                     parameterString.Append("&");
  345.                 }
  346.                 parameterString.Append(
  347.                     string.Format(
  348.                         CultureInfo.InvariantCulture,
  349.                         "{0}={1}",
  350.                         UrlEncode(item.Key),
  351.                         UrlEncode(item.Value)));
  352.             }
  353.             return UrlEncode(parameterString.ToString());
  354.         }
  355.         #endregion
  356.     }
  357. }</font>
复制代码

现在我们已经准备好了我们的脚本。 我们只需在按钮点击中调用“TwitterHandller”脚本和“TwitterHandller”脚本的“Twitter”()函数的TwitterLogin()函数。

感谢网友编译。
原文:How-to-Share-Image--Link-on-Twitter-through-Unity-iOS-App(地址:http://findnerd.com/list/view/Ho ... nity-iOS-App/34528/


作者: 毛毛虫    时间: 2018-11-2 15:56
真是好文章啊,谢谢分享




欢迎光临 纳金网 (http://course.narkii.com/club/) Powered by Discuz! X2.5