├── .gitignore
├── code
├── Launcher
│ ├── App.ico
│ ├── Launcher.csproj
│ ├── Launcher.settings.xml
│ └── Program.cs
├── Links
│ ├── LinkClient.cs
│ ├── LinkCrypto.cs
│ ├── LinkError.cs
│ ├── LinkEventArgs.cs
│ ├── LinkException.cs
│ ├── LinkExtension.cs
│ ├── LinkListener.cs
│ ├── LinkNotice.cs
│ ├── LinkNoticeSource.cs
│ ├── LinkPacket.cs
│ ├── Links.cs
│ ├── Links.csproj
│ └── LinksHelper.cs
├── Logger
│ ├── Log.cs
│ ├── LogListener.cs
│ └── Logger.csproj
├── Messenger.sln
└── Messenger
│ ├── App.ico
│ ├── App.manifest
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── Chatter.xaml
│ ├── Chatter.xaml.cs
│ ├── Commands.cs
│ ├── Connection.xaml
│ ├── Connection.xaml.cs
│ ├── ControlProfileImage.xaml
│ ├── ControlProfileImage.xaml.cs
│ ├── ControlShare.xaml
│ ├── ControlShare.xaml.cs
│ ├── ControlShareWorker.xaml
│ ├── ControlShareWorker.xaml.cs
│ ├── Controllers
│ ├── MessageController.cs
│ ├── ProfileController.cs
│ └── ShareController.cs
│ ├── Entrance.xaml
│ ├── Entrance.xaml.cs
│ ├── Extensions
│ ├── Extension.cs
│ └── NativeMethods.cs
│ ├── Framework.cs
│ ├── Messenger.csproj
│ ├── Models
│ ├── Host.cs
│ ├── IFinal.cs
│ ├── LoaderAttribute.cs
│ ├── LoaderFlags.cs
│ ├── Packet.cs
│ ├── Profile.cs
│ ├── RouteAttribute.cs
│ ├── Share.cs
│ ├── ShareBasic.cs
│ ├── ShareReceiver.cs
│ ├── ShareStatus.cs
│ └── ShareWorker.cs
│ ├── Modules
│ ├── CacheModule.cs
│ ├── EnvironmentModule.cs
│ ├── HistoryModule.cs
│ ├── HostModule.cs
│ ├── LinkModule.cs
│ ├── PostModule.cs
│ ├── ProfileModule.cs
│ ├── RouteModule.cs
│ ├── SettingModule.cs
│ └── ShareModule.cs
│ ├── PageClient.xaml
│ ├── PageClient.xaml.cs
│ ├── PageFrame.xaml
│ ├── PageFrame.xaml.cs
│ ├── PageGroups.xaml
│ ├── PageGroups.xaml.cs
│ ├── PageManager.cs
│ ├── PageOption.xaml
│ ├── PageOption.xaml.cs
│ ├── PageProfile.xaml
│ ├── PageProfile.xaml.cs
│ ├── PageRecent.xaml
│ ├── PageRecent.xaml.cs
│ ├── PageShare.xaml
│ ├── PageShare.xaml.cs
│ ├── Resources
│ ├── Collection.xaml
│ ├── Container.xaml
│ ├── General.xaml
│ ├── Geometry.xaml
│ └── Validation.xaml
│ ├── Shower.xaml
│ ├── Shower.xaml.cs
│ └── Tools
│ ├── ImageSourceConverter.cs
│ ├── LengthUnitConverter.cs
│ ├── LogicToPixelConverter.cs
│ ├── ObjectTypeConverter.cs
│ ├── ProfileHintConverter.cs
│ ├── ProfileIdValidation.cs
│ ├── ProfileLogoConverter.cs
│ ├── SocketPortValidation.cs
│ └── StringEmptyConverter.cs
└── image
├── 00.png
├── 01.png
├── 02.png
└── 03.png
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/code/Launcher/App.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/code/Launcher/App.ico
--------------------------------------------------------------------------------
/code/Launcher/Launcher.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | netcoreapp3.1;net472
6 | Launcher
7 | Launcher
8 | 8.0
9 | App.ico
10 | Messenger Project
11 | Mikodev
12 | Mikodev 2020
13 | 1.9.0
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Always
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/code/Launcher/Launcher.settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/code/Launcher/Program.cs:
--------------------------------------------------------------------------------
1 | using Mikodev.Logger;
2 | using Mikodev.Network;
3 | using System;
4 | using System.Collections;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Threading.Tasks;
8 | using System.Xml;
9 |
10 | namespace Launcher
11 | {
12 | internal class Program
13 | {
14 | private static async Task Main(string[] args)
15 | {
16 | Log.Run(nameof(Launcher) + ".log");
17 |
18 | try
19 | {
20 | var xml = new XmlDocument();
21 | xml.Load(nameof(Launcher) + ".settings.xml");
22 | var lst = xml.SelectNodes("/settings/setting[@key]");
23 | var dic = ((IEnumerable)lst)
24 | .Cast()
25 | .ToDictionary(r => r.SelectSingleNode("@key").Value, r => r.SelectSingleNode("@value").Value);
26 | var nam = dic["server-name"];
27 | var add = IPAddress.Parse(dic["listen-address"]);
28 | var max = int.Parse(dic["client-limits"]);
29 | var pot = int.Parse(dic["tcp-port"]);
30 | var bro = int.Parse(dic["udp-port"]);
31 | await LinkListener.Run(add, pot, bro, max, nam);
32 | }
33 | catch (Exception ex)
34 | {
35 | Log.Error(ex);
36 | }
37 |
38 | Log.Close();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/code/Links/LinkCrypto.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Security.Cryptography;
4 |
5 | namespace Mikodev.Network
6 | {
7 | internal static class LinkCrypto
8 | {
9 | internal const int _Key = 32;
10 |
11 | internal const int _Block = 16;
12 |
13 | internal readonly static Random s_ran = new Random();
14 |
15 | public static byte[] GetKey()
16 | {
17 | var buf = new byte[_Key];
18 | lock (s_ran)
19 | s_ran.NextBytes(buf);
20 | return buf;
21 | }
22 |
23 | public static byte[] GetBlock()
24 | {
25 | var buf = new byte[_Block];
26 | lock (s_ran)
27 | s_ran.NextBytes(buf);
28 | return buf;
29 | }
30 |
31 | public static byte[] Encrypt(this AesManaged aes, byte[] buf) => Transform(buf, 0, buf.Length, aes.CreateEncryptor());
32 |
33 | public static byte[] Decrypt(this AesManaged aes, byte[] buf) => Transform(buf, 0, buf.Length, aes.CreateDecryptor());
34 |
35 | internal static byte[] Transform(byte[] buffer, int offset, int count, ICryptoTransform tramsform)
36 | {
37 | var mst = new MemoryStream();
38 | using (var cst = new CryptoStream(mst, tramsform, CryptoStreamMode.Write))
39 | cst.Write(buffer, offset, count);
40 | return mst.ToArray();
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/code/Links/LinkError.cs:
--------------------------------------------------------------------------------
1 | namespace Mikodev.Network
2 | {
3 | public enum LinkError : int
4 | {
5 | None,
6 |
7 | Success,
8 |
9 | Overflow,
10 |
11 | ProtocolMismatch,
12 |
13 | IdInvalid,
14 |
15 | IdConflict,
16 |
17 | CountLimited,
18 |
19 | GroupLimited,
20 |
21 | QueueLimited,
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/code/Links/LinkEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Mikodev.Network
4 | {
5 | public class LinkEventArgs : EventArgs
6 | {
7 | internal T _obj;
8 |
9 | internal bool _cancel = false;
10 |
11 | internal bool _finish = false;
12 |
13 | public LinkEventArgs(T value) => _obj = value;
14 |
15 | public T Object => _obj;
16 |
17 | public bool Cancel { get => _cancel; set => _cancel = value; }
18 |
19 | public bool Finish { get => _finish; set => _finish = value; }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/code/Links/LinkException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace Mikodev.Network
5 | {
6 | [Serializable]
7 | public class LinkException : Exception
8 | {
9 | internal readonly LinkError _error = LinkError.None;
10 |
11 | public LinkException(LinkError error) : base(_GetMessage(error)) => _error = error;
12 |
13 | public LinkException(LinkError error, Exception inner) : base(_GetMessage(error), inner) => _error = error;
14 |
15 | protected LinkException(SerializationInfo info, StreamingContext context) : base(info, context)
16 | {
17 | if (info == null)
18 | throw new ArgumentNullException(nameof(info));
19 | _error = (LinkError)info.GetValue(nameof(LinkError), typeof(LinkError));
20 | }
21 |
22 | public override void GetObjectData(SerializationInfo info, StreamingContext context)
23 | {
24 | if (info == null)
25 | throw new ArgumentNullException(nameof(info));
26 | info.AddValue(nameof(LinkError), _error);
27 | base.GetObjectData(info, context);
28 | }
29 |
30 | internal static string _GetMessage(LinkError error)
31 | {
32 | switch (error)
33 | {
34 | case LinkError.Success:
35 | return "Operation successful without error!";
36 |
37 | case LinkError.Overflow:
38 | return "Buffer length overflow!";
39 |
40 | case LinkError.ProtocolMismatch:
41 | return "Protocol mismatch!";
42 |
43 | case LinkError.IdInvalid:
44 | return "Invalid client id!";
45 |
46 | case LinkError.IdConflict:
47 | return "Client id conflict with current users!";
48 |
49 | case LinkError.CountLimited:
50 | return "Client count has been limited!";
51 |
52 | case LinkError.GroupLimited:
53 | return "Group label is too many!";
54 |
55 | case LinkError.QueueLimited:
56 | return "Message queue full!";
57 |
58 | default:
59 | return "Unknown error!";
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/code/Links/LinkExtension.cs:
--------------------------------------------------------------------------------
1 | using Mikodev.Binary;
2 | using System;
3 | using System.Net;
4 | using System.Net.Sockets;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using static System.BitConverter;
8 |
9 | namespace Mikodev.Network
10 | {
11 | public static class LinkExtension
12 | {
13 | public static byte[] Concat(params byte[][] arrays)
14 | {
15 | var sum = 0;
16 | for (var i = 0; i < arrays.Length; i++)
17 | sum += arrays[i].Length;
18 | var arr = new byte[sum];
19 | var idx = 0;
20 | for (var i = 0; i < arrays.Length; i++)
21 | {
22 | var cur = arrays[i];
23 | var len = cur.Length;
24 | Buffer.BlockCopy(cur, 0, arr, idx, len);
25 | idx += len;
26 | }
27 | return arr;
28 | }
29 |
30 | public static Task ConnectAsyncEx(this Socket socket, EndPoint endpoint) => Task.Factory.FromAsync((arg, obj) => socket.BeginConnect(endpoint, arg, obj), socket.EndConnect, null);
31 |
32 | public static Task AcceptAsyncEx(this Socket socket) => Task.Factory.FromAsync(socket.BeginAccept, socket.EndAccept, null);
33 |
34 | public static int SetKeepAlive(this Socket socket, bool enable = true, uint before = Links.KeepAliveBefore, uint interval = Links.KeepAliveInterval)
35 | {
36 | if (enable == true && (before < 1 || interval < 1))
37 | throw new ArgumentOutOfRangeException("Keep alive argument out of range.");
38 | var val = new byte[sizeof(uint)];
39 | var res = Concat(GetBytes(1U), GetBytes(before), GetBytes(interval));
40 | _ = socket.IOControl(IOControlCode.KeepAliveValues, res, val);
41 | return ToInt32(val, 0);
42 | }
43 |
44 | public static async Task ReceiveAsyncExt(this Socket socket)
45 | {
46 | var buf = await ReceiveAsyncEx(socket, sizeof(int));
47 | var len = ToInt32(buf, 0);
48 | var res = await ReceiveAsyncEx(socket, len);
49 | return res;
50 | }
51 |
52 | public static async Task ReceiveAsyncEx(this Socket socket, int length)
53 | {
54 | if (length < 1 || length > Links.BufferLengthLimit)
55 | throw new LinkException(LinkError.Overflow);
56 | var offset = 0;
57 | var buffer = new byte[length];
58 | while (length > 0)
59 | {
60 | var res = await Task.Factory.FromAsync((a, s) => socket.BeginReceive(buffer, offset, length, SocketFlags.None, a, s), socket.EndReceive, null);
61 | if (res < 1)
62 | throw new SocketException((int)SocketError.ConnectionReset);
63 | offset += res;
64 | length -= res;
65 | }
66 | return buffer;
67 | }
68 |
69 | public static async Task SendAsyncExt(this Socket socket, byte[] buffer)
70 | {
71 | var len = GetBytes(buffer.Length);
72 | await SendAsyncEx(socket, len);
73 | await SendAsyncEx(socket, buffer);
74 | }
75 |
76 | public static Task SendAsyncEx(this Socket socket, byte[] buffer) => SendAsyncEx(socket, buffer, 0, buffer.Length);
77 |
78 | public static async Task SendAsyncEx(this Socket socket, byte[] buffer, int offset, int length)
79 | {
80 | while (length > 0)
81 | {
82 | var res = await Task.Factory.FromAsync((a, o) => socket.BeginSend(buffer, offset, length, SocketFlags.None, a, o), socket.EndSend, null);
83 | if (res < 1)
84 | throw new SocketException((int)SocketError.ConnectionReset);
85 | offset += res;
86 | length -= res;
87 | }
88 | }
89 |
90 | public static LinkPacket LoadValue(this LinkPacket src, byte[] buf)
91 | {
92 | var ori = new Token(LinksHelper.Generator, buf);
93 | src._buffer = buf;
94 | src._origin = ori;
95 | src._source = ori["source"].As();
96 | src._target = ori["target"].As();
97 | src._path = ori["path"].As();
98 | src._data = ori["data", nothrow: true];
99 | return src;
100 | }
101 |
102 | public static async Task TimeoutAfter(this Task task, string message = null, int milliseconds = Links.Timeout)
103 | {
104 | if (task == null)
105 | throw new ArgumentNullException(nameof(task));
106 | if (milliseconds < 0)
107 | throw new ArgumentOutOfRangeException(nameof(milliseconds));
108 | using (var src = new CancellationTokenSource())
109 | {
110 | var res = await Task.WhenAny(task, Task.Delay(milliseconds));
111 | if (res != task)
112 | throw (string.IsNullOrEmpty(message))
113 | ? new TimeoutException()
114 | : new TimeoutException(message);
115 | src.Cancel();
116 | await task;
117 | }
118 | }
119 |
120 | public static async Task TimeoutAfter(this Task task, string message = null, int milliseconds = Links.Timeout)
121 | {
122 | if (task == null)
123 | throw new ArgumentNullException(nameof(task));
124 | if (milliseconds < 0)
125 | throw new ArgumentOutOfRangeException(nameof(milliseconds));
126 | using (var src = new CancellationTokenSource())
127 | {
128 | var res = await Task.WhenAny(task, Task.Delay(milliseconds));
129 | if (res != task)
130 | throw (string.IsNullOrEmpty(message))
131 | ? new TimeoutException()
132 | : new TimeoutException(message);
133 | src.Cancel();
134 | return await task;
135 | }
136 | }
137 |
138 | public static void AssertFatal(this bool result, string message)
139 | {
140 | if (result)
141 | return;
142 | Environment.FailFast(message);
143 | }
144 |
145 | public static void AssertFatal(this bool result, bool assert, string message)
146 | {
147 | if (result && assert)
148 | return;
149 | Environment.FailFast(message);
150 | }
151 |
152 | public static void AssertError(this LinkError error)
153 | {
154 | if (error == LinkError.Success)
155 | return;
156 | throw new LinkException(error);
157 | }
158 |
159 | ///
160 | /// 显式放弃等待该任务
161 | ///
162 | public static void Ignore(this T task) where T : Task
163 | {
164 | return;
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/code/Links/LinkNotice.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Mikodev.Network
5 | {
6 | public class LinkNotice
7 | {
8 | internal readonly LinkNoticeSource _source;
9 |
10 | internal readonly int _value;
11 |
12 | internal int _status;
13 |
14 | public bool IsAny => _status > 0;
15 |
16 | internal LinkNotice() => _status = 0;
17 |
18 | internal LinkNotice(LinkNoticeSource inspector, int version)
19 | {
20 | _source = inspector;
21 | _value = version;
22 | _status = 1;
23 | }
24 |
25 | public void Handled()
26 | {
27 | // 阻止多次调用
28 | if (Interlocked.CompareExchange(ref _status, 2, 1) != 1)
29 | throw new InvalidOperationException("Invalid operation!");
30 | _source._Handled(_value);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/code/Links/LinkNoticeSource.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Mikodev.Network
5 | {
6 | public class LinkNoticeSource
7 | {
8 | internal readonly TimeSpan _interval;
9 |
10 | internal DateTime _timestamp = DateTime.MinValue;
11 |
12 | internal int _version = 0;
13 |
14 | internal int _handled = 0;
15 |
16 | public LinkNoticeSource(TimeSpan interval)
17 | {
18 | if (interval < TimeSpan.Zero)
19 | throw new ArgumentOutOfRangeException(nameof(interval));
20 | _interval = interval;
21 | }
22 |
23 | public void Update()
24 | {
25 | _ = Interlocked.Increment(ref _version);
26 | }
27 |
28 | public LinkNotice Notice() => GetNotice(false);
29 |
30 | public LinkNotice GetNotice(bool force)
31 | {
32 | var ver = Volatile.Read(ref _version);
33 | var cur = _handled;
34 | if (cur == ver)
35 | goto nothing;
36 |
37 | if (force)
38 | return new LinkNotice(this, _version);
39 |
40 | var old = _timestamp;
41 | var now = DateTime.Now;
42 | var sub = now - old;
43 | if (sub > TimeSpan.Zero && sub < _interval)
44 | goto nothing;
45 | return new LinkNotice(this, ver);
46 |
47 | nothing:
48 | return new LinkNotice();
49 | }
50 |
51 | internal void _Handled(int version)
52 | {
53 | var now = DateTime.Now;
54 | _timestamp = now;
55 | Volatile.Write(ref _handled, version);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/code/Links/LinkPacket.cs:
--------------------------------------------------------------------------------
1 | using Mikodev.Binary;
2 |
3 | namespace Mikodev.Network
4 | {
5 | public class LinkPacket
6 | {
7 | internal int _source = 0;
8 |
9 | internal int _target = 0;
10 |
11 | internal string _path = null;
12 |
13 | internal byte[] _buffer = null;
14 |
15 | internal Token _origin = null;
16 |
17 | internal Token _data = null;
18 |
19 | public int Source => _source;
20 |
21 | public int Target => _target;
22 |
23 | public string Path => _path;
24 |
25 | public byte[] Buffer => _buffer;
26 |
27 | public Token Origin => _origin;
28 |
29 | public Token Data => _data;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/code/Links/Links.cs:
--------------------------------------------------------------------------------
1 | namespace Mikodev.Network
2 | {
3 | public static class Links
4 | {
5 | public const string Protocol = "mikodev.messenger.v1.20";
6 |
7 | public const int Port = 7550;
8 |
9 | public const int BroadcastPort = Port;
10 |
11 | public const int Timeout = 5 * 1000;
12 |
13 | public const int KeepAliveBefore = 300 * 1000;
14 |
15 | public const int KeepAliveInterval = Timeout;
16 |
17 | public const int Id = 0;
18 |
19 | public const int DefaultId = 0x7FFFFFFF;
20 |
21 | public const int ServerSocketLimit = 256;
22 |
23 | public const int ClientSocketLimit = 64;
24 |
25 | public const int BufferLength = 4 * 1024;
26 |
27 | public const int BufferLengthLimit = 1024 * 1024;
28 |
29 | public const int BufferQueueLimit = 16 * 1024 * 1024;
30 |
31 | public const int Delay = 4;
32 |
33 | public const int GroupLabelLimit = 32;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/code/Links/Links.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Mikodev.Links
6 | Mikodev.Network
7 | Messenger Project
8 | Mikodev
9 | Mikodev 2020
10 | 1.9.0
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/code/Links/LinksHelper.cs:
--------------------------------------------------------------------------------
1 | namespace Mikodev.Network
2 | {
3 | public static class LinksHelper
4 | {
5 | public static readonly Binary.IGenerator Generator = Binary.Generator.CreateDefault();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/code/Logger/Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Runtime.CompilerServices;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Mikodev.Logger
11 | {
12 | public static class Log
13 | {
14 | private const int _MaxQueueLength = 256;
15 |
16 | ///
17 | /// 日志固定前缀 (防止循环记录日志)
18 | ///
19 | internal static readonly string _prefix = $"[{nameof(Logger)}]";
20 |
21 | internal static readonly object s_filelocker = new object();
22 |
23 | internal static readonly Queue s_queue = new Queue();
24 |
25 | internal static TraceListener s_listener = null;
26 |
27 | internal static StreamWriter s_writer = null;
28 |
29 | internal static CancellationTokenSource s_cancel = null;
30 |
31 | internal static Task s_task = null;
32 |
33 | public static void Run(string path)
34 | {
35 | lock (s_filelocker)
36 | {
37 | if (s_listener is null)
38 | _ = Trace.Listeners.Add(s_listener = new LogListener());
39 |
40 | s_writer?.Dispose();
41 | var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
42 | s_writer = new StreamWriter(stream, Encoding.UTF8, 1024, false);
43 |
44 | if (s_cancel is null && s_task is null)
45 | {
46 | var cancel = new CancellationTokenSource();
47 | s_task = Task.Run(new Func(() => _Monitor(cancel.Token)));
48 | s_cancel = cancel;
49 | }
50 | }
51 | }
52 |
53 | public static void Close()
54 | {
55 | lock (s_filelocker)
56 | {
57 | var tsk = s_task;
58 | var src = s_cancel;
59 | var wtr = s_writer;
60 |
61 | if (tsk is null || src is null || wtr is null)
62 | return;
63 |
64 | src.Cancel();
65 | tsk.Wait();
66 | src.Dispose();
67 | wtr.Dispose();
68 |
69 | s_task = null;
70 | s_cancel = null;
71 | s_writer = null;
72 | }
73 | }
74 |
75 | private static async Task _Monitor(CancellationToken token)
76 | {
77 | while (true)
78 | {
79 | var arr = default(string[]);
80 | lock (s_queue)
81 | {
82 | arr = s_queue.ToArray();
83 | s_queue.Clear();
84 | }
85 |
86 | if (arr.Length < 1)
87 | {
88 | if (token.IsCancellationRequested)
89 | return;
90 | await Task.Delay(4);
91 | continue;
92 | }
93 |
94 | try
95 | {
96 | lock (s_filelocker)
97 | {
98 | var wtr = s_writer;
99 | foreach (var i in arr)
100 | wtr.Write(i);
101 | wtr.Flush();
102 | }
103 | }
104 | catch (Exception ex)
105 | {
106 | _InternalError(ex.ToString());
107 | }
108 | }
109 | }
110 |
111 | private static void _Enqueue(string msg)
112 | {
113 | lock (s_queue)
114 | {
115 | var len = s_queue.Count + 1;
116 | var sub = len - _MaxQueueLength;
117 | if (sub > 0)
118 | {
119 | for (var i = 0; i < len; i++)
120 | _ = s_queue.Dequeue();
121 | _InternalError("Log queue full!");
122 | }
123 | s_queue.Enqueue(msg);
124 | }
125 | }
126 |
127 | ///
128 | /// 记录异常 (如果异常为空则不记录)
129 | ///
130 | public static void Error(Exception err, [CallerMemberName] string name = null, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0)
131 | {
132 | if (err == null)
133 | return;
134 | while (err is AggregateException a && a.InnerExceptions?.Count == 1 && a.InnerException is Exception val)
135 | err = val;
136 | Info(err.ToString(), name, file, line);
137 | }
138 |
139 | ///
140 | /// 记录自定义消息 (如果异常为空则不记录)
141 | ///
142 | public static void Info(string message, [CallerMemberName] string name = null, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0)
143 | {
144 | if (message == null)
145 | return;
146 | var lbr = Environment.NewLine;
147 |
148 | var msg = $"[时间: {DateTime.Now:u}]" + lbr +
149 | $"[文件: {file}]" + lbr +
150 | $"[行号: {line}]" + lbr +
151 | $"[方法: {name}]" + lbr +
152 | $"{message}" + lbr + lbr;
153 |
154 | Trace.Write(_prefix + Environment.NewLine + msg);
155 | _Enqueue(msg);
156 | }
157 |
158 | internal static void _InternalError(string msg)
159 | {
160 | Trace.WriteLine(_prefix + Environment.NewLine + msg);
161 | }
162 |
163 | internal static void _Trace(string txt)
164 | {
165 | if (string.IsNullOrEmpty(txt) || txt.StartsWith(_prefix))
166 | return;
167 |
168 | var lbr = Environment.NewLine;
169 | var msg = $"[时间: {DateTime.Now:u}]" + lbr +
170 | $"[来源: {nameof(Trace)}]" + lbr +
171 | $"{txt}" + lbr + lbr;
172 | _Enqueue(msg);
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/code/Logger/LogListener.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace Mikodev.Logger
4 | {
5 | internal class LogListener : TraceListener
6 | {
7 | public override void Write(string message) => Log._Trace(message);
8 |
9 | public override void WriteLine(string message) => Log._Trace(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/code/Logger/Logger.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Mikodev.Logger
6 | Mikodev.Network
7 | Messenger Project
8 | Mikodev
9 | Mikodev 2020
10 | 1.9.0
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/code/Messenger.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27004.2005
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Messenger", "Messenger\Messenger.csproj", "{138B6748-7828-4442-A574-EBCF632A44B0}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{875067CE-5717-451E-AEDB-22FDF3AD3C03}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Links", "Links\Links.csproj", "{A3ECBE4A-2D6A-490D-B8B5-6768A1253213}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Logger\Logger.csproj", "{F9128A2A-36B1-41BB-AE3C-6D2E93F66605}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {138B6748-7828-4442-A574-EBCF632A44B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {138B6748-7828-4442-A574-EBCF632A44B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {138B6748-7828-4442-A574-EBCF632A44B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {138B6748-7828-4442-A574-EBCF632A44B0}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {F1C69CC5-C87B-412B-97C2-53F52C9652AC}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/code/Messenger/App.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/code/Messenger/App.ico
--------------------------------------------------------------------------------
/code/Messenger/App.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
51 |
58 |
59 |
60 |
61 |
62 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/code/Messenger/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/code/Messenger/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Mikodev.Logger;
2 | using System;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using System.Windows.Input;
6 |
7 | namespace Messenger
8 | {
9 | public partial class App : Application
10 | {
11 | public event EventHandler TextBoxKeyDown;
12 |
13 | protected override void OnStartup(StartupEventArgs e)
14 | {
15 | base.OnStartup(e);
16 |
17 | DispatcherUnhandledException += (s, arg) =>
18 | {
19 | arg.Handled = true;
20 | MessageBox.Show(arg.Exception.ToString(), "Unhandled Exception", MessageBoxButton.OK, MessageBoxImage.Error);
21 | Shutdown(1);
22 | };
23 |
24 | void _Close(object sender, EventArgs args)
25 | {
26 | Framework.Close();
27 | Log.Close();
28 | }
29 |
30 | Exit += _Close;
31 | SessionEnding += _Close;
32 |
33 | Log.Run(nameof(Messenger) + ".log");
34 | EventManager.RegisterClassHandler(typeof(TextBox), UIElement.KeyDownEvent, new KeyEventHandler((s, arg) => TextBoxKeyDown?.Invoke(s, arg)));
35 | Framework.Start();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/code/Messenger/Chatter.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Models;
3 | using Messenger.Modules;
4 | using Microsoft.Win32;
5 | using Mikodev.Logger;
6 | using Mikodev.Network;
7 | using System;
8 | using System.ComponentModel;
9 | using System.IO;
10 | using System.Windows;
11 | using System.Windows.Controls;
12 | using System.Windows.Input;
13 |
14 | namespace Messenger
15 | {
16 | ///
17 | /// Interaction logic for Chatter.xaml
18 | ///
19 | public partial class Chatter : Page
20 | {
21 | private Profile _profile = null;
22 |
23 | private BindingList _messages = null;
24 |
25 | public Profile Profile => _profile;
26 |
27 | public Chatter()
28 | {
29 | InitializeComponent();
30 | Loaded += _Loaded;
31 | Unloaded += _Unloaded;
32 | }
33 |
34 | private void _Loaded(object sender, RoutedEventArgs e)
35 | {
36 | HistoryModule.Receive += _HistoryReceiving;
37 | (Application.Current as App).TextBoxKeyDown += _TextBoxKeyDown;
38 |
39 | _profile = ProfileModule.Inscope;
40 | _messages = _profile.GetMessages();
41 | uiProfileGrid.DataContext = _profile;
42 | uiMessageBox.ItemsSource = _messages;
43 | _messages.ListChanged += _ListChanged;
44 | uiMessageBox.ScrollIntoLastEx();
45 | }
46 |
47 | private void _Unloaded(object sender, RoutedEventArgs e)
48 | {
49 | uiMessageBox.ItemsSource = null;
50 | HistoryModule.Receive -= _HistoryReceiving;
51 | (Application.Current as App).TextBoxKeyDown -= _TextBoxKeyDown;
52 | _messages.ListChanged -= _ListChanged;
53 | }
54 |
55 | private void _ListChanged(object sender, ListChangedEventArgs e)
56 | {
57 | if (e.ListChangedType != ListChangedType.ItemAdded)
58 | return;
59 | uiMessageBox.ScrollIntoLastEx();
60 | }
61 |
62 | ///
63 | /// 拦截消息通知
64 | ///
65 | private void _HistoryReceiving(object sender, LinkEventArgs e)
66 | {
67 | if (e.Object.Index != _profile.Id)
68 | return;
69 | e.Finish = true;
70 | }
71 |
72 | private void _TextBoxKeyDown(object sender, KeyEventArgs e)
73 | {
74 | if (sender != uiInputBox || e.Key != Key.Enter)
75 | return;
76 | var mod = e.KeyboardDevice.Modifiers;
77 | var ins = SettingModule.Instance;
78 | if (ins.UseControlEnter && mod == ModifierKeys.Control || ins.UseEnter && mod == ModifierKeys.None)
79 | _SendText();
80 | else
81 | uiInputBox.InsertEx(Environment.NewLine);
82 | e.Handled = true;
83 | }
84 |
85 | private void _Click(object sender, RoutedEventArgs e)
86 | {
87 | var tag = (e.OriginalSource as Button)?.Tag as string;
88 | if (tag == null)
89 | return;
90 | if (tag == "text")
91 | _SendText();
92 | else if (tag == "image")
93 | _PushImage();
94 | else if (tag == "clean")
95 | HistoryModule.Clear(_profile.Id);
96 | _ = uiInputBox.Focus();
97 | }
98 |
99 | private void _SendText()
100 | {
101 | var str = uiInputBox.Text.TrimEnd(new char[] { '\0', '\r', '\n', '\t', ' ' });
102 | if (str.Length < 1)
103 | return;
104 | uiInputBox.Text = string.Empty;
105 | PostModule.Text(_profile.Id, str);
106 | ProfileModule.SetRecent(_profile);
107 | }
108 |
109 | private void _PushImage()
110 | {
111 | var ofd = new OpenFileDialog() { Filter = "位图文件|*.bmp;*.png;*.jpg" };
112 | if (ofd.ShowDialog() != true)
113 | return;
114 | try
115 | {
116 | var buf = CacheModule.ImageZoom(ofd.FileName);
117 | PostModule.Image(_profile.Id, buf);
118 | ProfileModule.SetRecent(_profile);
119 | }
120 | catch (Exception ex)
121 | {
122 | Log.Error(ex);
123 | Entrance.ShowError("发送图片失败", ex);
124 | }
125 | }
126 |
127 | private void _Share(string path)
128 | {
129 | if (File.Exists(path))
130 | PostModule.File(_profile.Id, path);
131 | else if (Directory.Exists(path))
132 | PostModule.Directory(_profile.Id, path);
133 | return;
134 | }
135 |
136 | private void _TextBoxPreviewDragOver(object sender, DragEventArgs e)
137 | {
138 | if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
139 | e.Effects = DragDropEffects.None;
140 | else
141 | e.Effects = DragDropEffects.Copy;
142 | e.Handled = true;
143 | }
144 |
145 | private void _TextBoxPreviewDrop(object sender, DragEventArgs e)
146 | {
147 | if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
148 | return;
149 | var arr = e.Data.GetData(DataFormats.FileDrop) as string[];
150 | if (arr == null || arr.Length < 1)
151 | return;
152 | var val = arr[0];
153 | _Share(val);
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/code/Messenger/Commands.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Logger;
4 | using System;
5 | using System.Diagnostics;
6 | using System.Windows;
7 | using System.Windows.Input;
8 |
9 | namespace Messenger
10 | {
11 | internal static class Commands
12 | {
13 | public static RoutedUICommand CopyText { get; } = new RoutedUICommand() { Text = "复制消息内容" };
14 |
15 | public static RoutedUICommand Remove { get; } = new RoutedUICommand() { Text = "移除这条消息" };
16 |
17 | public static RoutedUICommand ViewImage { get; } = new RoutedUICommand() { Text = "在图片查看器中查看" };
18 |
19 | static Commands()
20 | {
21 | var cpy = new CommandBinding { Command = CopyText };
22 | cpy.CanExecute += _CopyCanExecute;
23 | cpy.Executed += _CopyExecuted;
24 |
25 | var rmv = new CommandBinding { Command = Remove };
26 | rmv.CanExecute += _RemoveCanExecute;
27 | rmv.Executed += _RemoveExecuted;
28 |
29 | var vie = new CommandBinding { Command = ViewImage };
30 | vie.CanExecute += _ViewImageCanExecute;
31 | vie.Executed += _ViewImageExecuted;
32 |
33 | var list = Application.Current.MainWindow.CommandBindings;
34 | _ = list.Add(cpy);
35 | _ = list.Add(rmv);
36 | _ = list.Add(vie);
37 | }
38 |
39 | private static void _ViewImageCanExecute(object sender, CanExecuteRoutedEventArgs e)
40 | {
41 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
42 | if (msg is null || msg.Path != "image")
43 | e.CanExecute = false;
44 | else
45 | e.CanExecute = true;
46 | e.Handled = true;
47 | }
48 |
49 | private static void _ViewImageExecuted(object sender, ExecutedRoutedEventArgs e)
50 | {
51 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
52 | if (msg is null)
53 | return;
54 | var str = msg.Object as string;
55 | if (str == null)
56 | return;
57 | try
58 | {
59 | var flp = CacheModule.GetPath(str);
60 | _ = Process.Start(flp);
61 | }
62 | catch (Exception ex)
63 | {
64 | Log.Error(ex);
65 | }
66 | }
67 |
68 | private static void _RemoveCanExecute(object sender, CanExecuteRoutedEventArgs e)
69 | {
70 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
71 | if (msg is null)
72 | e.CanExecute = false;
73 | else
74 | e.CanExecute = true;
75 | e.Handled = true;
76 | }
77 |
78 | private static void _RemoveExecuted(object sender, ExecutedRoutedEventArgs e)
79 | {
80 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
81 | if (msg is null)
82 | return;
83 | HistoryModule.Remove(msg);
84 | e.Handled = true;
85 | }
86 |
87 | private static void _CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
88 | {
89 | var val = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
90 | if (val == null || val.Path != "text")
91 | e.CanExecute = false;
92 | else
93 | e.CanExecute = true;
94 | e.Handled = true;
95 | }
96 |
97 | private static void _CopyExecuted(object sender, ExecutedRoutedEventArgs e)
98 | {
99 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet;
100 | if (msg?.MessageText is null)
101 | return;
102 | try
103 | {
104 | Clipboard.SetText(msg.MessageText);
105 | }
106 | catch (Exception ex)
107 | {
108 | Log.Error(ex);
109 | Entrance.ShowError("复制消息出错", ex);
110 | }
111 | e.Handled = true;
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/code/Messenger/Connection.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 连接
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
107 |
108 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/code/Messenger/Connection.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Logger;
4 | using System;
5 | using System.ComponentModel;
6 | using System.Linq;
7 | using System.Net;
8 | using System.Net.Sockets;
9 | using System.Threading.Tasks;
10 | using System.Windows;
11 | using System.Windows.Controls;
12 | using System.Windows.Navigation;
13 |
14 | namespace Messenger
15 | {
16 | ///
17 | /// Interaction logic for Connection.xaml
18 | ///
19 | public partial class Connection : Page
20 | {
21 | private class _Temp
22 | {
23 | public string I { get; set; } = string.Empty;
24 |
25 | public string P { get; set; } = string.Empty;
26 | }
27 |
28 | private readonly BindingList _hosts = new BindingList();
29 |
30 | public Connection()
31 | {
32 | InitializeComponent();
33 | uiTableGrid.DataContext = new _Temp();
34 | Loaded += _Loaded;
35 | }
36 |
37 | private void _Loaded(object sender, RoutedEventArgs e)
38 | {
39 | if (HostModule.Name != null)
40 | {
41 | uiHostBox.Text = HostModule.Name;
42 | uiPortBox.Text = HostModule.Port.ToString();
43 | }
44 | uiIdBox.Text = ProfileModule.Id.ToString();
45 | uiServerList.ItemsSource = _hosts;
46 | uiServerList.SelectionChanged += _SelectionChanged;
47 | }
48 |
49 | private void _SelectionChanged(object sender, SelectionChangedEventArgs e)
50 | {
51 | if (e.AddedItems.Count < 1)
52 | return;
53 | var itm = e.AddedItems[0] as Host;
54 | if (itm == null)
55 | return;
56 | uiHostBox.Text = itm.Address?.ToString();
57 | uiPortBox.Text = itm.Port.ToString();
58 | uiServerList.SelectedIndex = -1;
59 | }
60 |
61 | private async void _Click(object sender, RoutedEventArgs e)
62 | {
63 | async void _Refresh()
64 | {
65 | uiRefreshButton.IsEnabled = false;
66 | var lst = await Task.Run(HostModule.Refresh);
67 | foreach (var inf in lst)
68 | {
69 | var idx = _hosts.IndexOf(inf);
70 | if (idx < 0)
71 | _hosts.Add(inf);
72 | else _hosts[idx] = inf;
73 | }
74 | uiRefreshButton.IsEnabled = true;
75 | }
76 |
77 | var src = (Button)e.OriginalSource;
78 | if (src == uiBrowserButton)
79 | {
80 | uiBrowserButton.Visibility = Visibility.Collapsed;
81 | uiRefreshButton.Visibility =
82 | uiListGrid.Visibility = Visibility.Visible;
83 | _Refresh();
84 | return;
85 | }
86 | else if (src == uiRefreshButton)
87 | {
88 | _hosts.Clear();
89 | _Refresh();
90 | return;
91 | }
92 | else if (src == uiConnectButton)
93 | {
94 | uiConnectButton.IsEnabled = false;
95 | try
96 | {
97 | var uid = int.Parse(uiIdBox.Text);
98 | var pot = int.Parse(uiPortBox.Text);
99 | var hos = uiHostBox.Text;
100 |
101 | var add = IPAddress.TryParse(hos, out var hst);
102 | if (add == false)
103 | hst = Dns.GetHostEntry(hos).AddressList.First(r => r.AddressFamily == AddressFamily.InterNetwork);
104 | var iep = new IPEndPoint(hst, pot);
105 |
106 | // 放弃等待该方法返回的任务
107 | _ = await LinkModule.Start(uid, iep);
108 | HostModule.Name = hos;
109 | HostModule.Port = pot;
110 |
111 | _ = NavigationService.Navigate(new PageFrame());
112 | }
113 | catch (Exception ex)
114 | {
115 | Log.Error(ex);
116 | Entrance.ShowError("连接失败", ex);
117 | }
118 | uiConnectButton.IsEnabled = true;
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/code/Messenger/ControlProfileImage.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/code/Messenger/ControlProfileImage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows.Controls;
2 |
3 | namespace Messenger
4 | {
5 | ///
6 | /// ControlProfileImage.xaml 的交互逻辑
7 | ///
8 | public partial class ControlProfileImage : UserControl
9 | {
10 | public ControlProfileImage()
11 | {
12 | InitializeComponent();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/code/Messenger/ControlShare.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/code/Messenger/ControlShare.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger
6 | {
7 | ///
8 | /// ControlShare.xaml 的交互逻辑
9 | ///
10 | public partial class ControlShare : UserControl
11 | {
12 | public ControlShare()
13 | {
14 | InitializeComponent();
15 | }
16 |
17 | private void _Click(object sender, RoutedEventArgs e)
18 | {
19 | var btn = e.OriginalSource as Button;
20 | if (btn == null)
21 | return;
22 | var tag = btn.Tag as string;
23 | var dis = btn.DataContext as IDisposable;
24 | if (dis == null || tag == null)
25 | return;
26 | else if (tag == "stop")
27 | dis.Dispose();
28 | return;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/code/Messenger/ControlShareWorker.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using System;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 |
6 | namespace Messenger
7 | {
8 | ///
9 | /// ControlShareWorker.xaml 的交互逻辑
10 | ///
11 | public partial class ControlShareWorker : UserControl
12 | {
13 | public ControlShareWorker()
14 | {
15 | InitializeComponent();
16 | }
17 |
18 | private void _Click(object sender, RoutedEventArgs e)
19 | {
20 | var btn = e.OriginalSource as Button;
21 | if (btn == null)
22 | return;
23 | var dat = btn.DataContext;
24 | var tag = btn.Tag as string;
25 | if (dat == null || tag == null)
26 | return;
27 | if (tag == "play" && dat is ShareReceiver rec)
28 | _ = rec.Start();
29 | else if (tag == "stop" && dat is IDisposable dis)
30 | dis.Dispose();
31 | return;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/code/Messenger/Controllers/MessageController.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Logger;
4 | using Mikodev.Network;
5 |
6 | namespace Messenger.Controllers
7 | {
8 | ///
9 | /// 消息处理
10 | ///
11 | public class MessageController : LinkPacket
12 | {
13 | ///
14 | /// 文本消息
15 | ///
16 | [Route("msg.text")]
17 | public void Text()
18 | {
19 | var txt = Data.As();
20 | _ = HistoryModule.Insert(Source, Target, "text", txt);
21 | }
22 |
23 | ///
24 | /// 图片消息
25 | ///
26 | [Route("msg.image")]
27 | public void Image()
28 | {
29 | var buf = Data.As();
30 | _ = HistoryModule.Insert(Source, Target, "image", buf);
31 | }
32 |
33 | ///
34 | /// 提示信息
35 | ///
36 | [Route("msg.notice")]
37 | public void Notice()
38 | {
39 | var typ = Data["type"].As();
40 | var par = Data["parameter"].As();
41 | var str = typ == "share.file"
42 | ? $"已成功接收文件 {par}"
43 | : typ == "share.dir"
44 | ? $"已成功接收文件夹 {par}"
45 | : null;
46 | if (str == null)
47 | Log.Info($"Unknown notice type: {typ}, parameter: {par}");
48 | else
49 | _ = HistoryModule.Insert(Source, Target, "notice", str);
50 | return;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/code/Messenger/Controllers/ProfileController.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Network;
4 |
5 | namespace Messenger.Controllers
6 | {
7 | ///
8 | /// 处理用户信息
9 | ///
10 | public class ProfileController : LinkPacket
11 | {
12 | ///
13 | /// 向发送者返回本机的用户信息
14 | ///
15 | [Route("user.request")]
16 | public void Request()
17 | {
18 | PostModule.UserProfile(Source);
19 | }
20 |
21 | ///
22 | /// 处理传入的用户信息
23 | ///
24 | [Route("user.profile")]
25 | public void Profile()
26 | {
27 | var cid = Data["id"].As();
28 | var pro = new Profile(cid)
29 | {
30 | Name = Data["name"].As(),
31 | Text = Data["text"].As(),
32 | };
33 |
34 | var buf = Data["image"].As();
35 | if (buf.Length > 0)
36 | pro.Image = CacheModule.SetBuffer(buf, true);
37 | ProfileModule.Insert(pro);
38 | }
39 |
40 | ///
41 | /// 处理服务器返回的用户 Id 列表
42 | ///
43 | [Route("user.list")]
44 | public void List()
45 | {
46 | var lst = Data.As();
47 | _ = ProfileModule.Remove(lst);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/code/Messenger/Controllers/ShareController.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Network;
4 |
5 | namespace Messenger.Controllers
6 | {
7 | ///
8 | /// 处理共享信息
9 | ///
10 | public class ShareController : LinkPacket
11 | {
12 | [Route("share.info")]
13 | public void Take()
14 | {
15 | var rec = new ShareReceiver(Source, Data);
16 | ShareModule.Register(rec);
17 | _ = HistoryModule.Insert(Source, Target, "share", rec);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/code/Messenger/Entrance.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using Microsoft.Win32;
3 | using System;
4 | using System.ComponentModel;
5 | using System.Windows;
6 | using System.Windows.Controls;
7 | using System.Windows.Input;
8 | using System.Windows.Interop;
9 | using System.Windows.Media;
10 | using static Messenger.Extensions.NativeMethods;
11 | using static System.Windows.ResizeMode;
12 | using static System.Windows.WindowState;
13 |
14 | namespace Messenger
15 | {
16 | ///
17 | /// Interaction logic for Entrance.xaml
18 | ///
19 | public partial class Entrance : Window
20 | {
21 | public Entrance()
22 | {
23 | InitializeComponent();
24 | Closing += _Closing;
25 | }
26 |
27 | private void _Closing(object sender, CancelEventArgs e)
28 | {
29 | if (LinkModule.IsRunning == false)
30 | return;
31 | if (WindowState != Minimized)
32 | WindowState = Minimized;
33 | e.Cancel = true;
34 | }
35 |
36 | private void _Click(object sender, RoutedEventArgs e)
37 | {
38 | var tag = (e.OriginalSource as Button)?.Tag as string;
39 | if (tag == "confirm")
40 | uiMessagePanel.Visibility = Visibility.Collapsed;
41 | return;
42 | }
43 |
44 | ///
45 | /// 显示提示信息 (可以跨线程调用)
46 | ///
47 | /// 标题
48 | /// 内容 (调用 方法)
49 | public static void ShowError(string title, Exception content)
50 | {
51 | var app = Application.Current;
52 | var dis = app.Dispatcher;
53 | dis.Invoke(() =>
54 | {
55 | var win = app.MainWindow as Entrance;
56 | if (win == null)
57 | return;
58 | win.uiHeadText.Text = title;
59 |
60 | win.uiContentText.Text = content?.ToString() ?? "未提供信息";
61 | win.uiMessagePanel.Visibility = Visibility.Visible;
62 | });
63 | }
64 |
65 | #region Flat window style
66 |
67 | private void _BorderLoaded(object sender, RoutedEventArgs e)
68 | {
69 | // 隐藏窗体控制按钮
70 | var han = new WindowInteropHelper(this).Handle;
71 | var now = GetWindowLong(han, GWL_STYLE);
72 | var res = SetWindowLong(han, GWL_STYLE, now & ~WS_SYSMENU);
73 |
74 | // 若为低版本的 Windows, 设置边框颜色为灰色
75 | var src = (Border)e.OriginalSource;
76 | var ver = Environment.OSVersion.Version;
77 | if (ver.Major < 6 || ver.Major == 6 && ver.Minor < 2)
78 | src.Background = Brushes.Gray;
79 | return;
80 | }
81 |
82 | private void _PanelClick(object sender, RoutedEventArgs e)
83 | {
84 | var tag = (e.OriginalSource as Button)?.Tag as string;
85 | if (tag == "min")
86 | WindowState = Minimized;
87 | else if (tag == "max")
88 | _Toggle();
89 | else if (tag == "exit")
90 | Close();
91 | return;
92 | }
93 |
94 | private void _Toggle()
95 | {
96 | if (ResizeMode != CanResize && ResizeMode != CanResizeWithGrip)
97 | return;
98 | var cur = WindowState;
99 | if (cur == Maximized && _IsTabletMode())
100 | return;
101 | WindowState = (cur == Maximized) ? Normal : Maximized;
102 | }
103 |
104 | private void _MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
105 | {
106 | if (e.ClickCount == 2)
107 | _Toggle();
108 | else
109 | DragMove();
110 | return;
111 | }
112 |
113 | private const string _TabletModeKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ImmersiveShell";
114 |
115 | private const string _TabletModeValue = "TabletMode";
116 |
117 | private bool _IsTabletMode()
118 | {
119 | var val = Registry.GetValue(_TabletModeKey, _TabletModeValue, -1);
120 | return val.Equals(1);
121 | }
122 |
123 | #endregion
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/code/Messenger/Extensions/NativeMethods.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace Messenger.Extensions
5 | {
6 | internal static class NativeMethods
7 | {
8 | public const int GWL_STYLE = -16;
9 |
10 | public const int WS_SYSMENU = 0x80000;
11 |
12 | [DllImport("user32.dll", SetLastError = true)]
13 | public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
14 |
15 | [DllImport("user32.dll")]
16 | public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
17 |
18 | [DllImport("user32.dll")]
19 | internal static extern bool FlashWindow(IntPtr handle, bool invert);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/code/Messenger/Framework.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Models;
3 | using Mikodev.Logger;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Messenger
11 | {
12 | public sealed class Framework : IDisposable
13 | {
14 | private Framework() { }
15 |
16 | private static readonly Framework s_ins = new Framework();
17 |
18 | private readonly object _locker = new object();
19 |
20 | private readonly CancellationTokenSource _cancel = new CancellationTokenSource();
21 |
22 | private bool _started = false;
23 |
24 | private bool _closed = false;
25 |
26 | private List _exit;
27 |
28 | private void _Start()
29 | {
30 | // 利用反射运行模块
31 | var lst = Extension.FindAttribute(
32 | typeof(LoaderAttribute).Assembly,
33 | typeof(LoaderAttribute), null,
34 | (a, m, t) => new { attribute = (LoaderAttribute)a, method = m, path = $"{t.FullName}.{m.Name}" }
35 | ).ToList();
36 |
37 | var loa = from r in lst
38 | where r.attribute.Flag == LoaderFlags.OnLoad
39 | orderby r.attribute.Level
40 | select (Action)Delegate.CreateDelegate(typeof(Action), r.method);
41 |
42 | var ext = from r in lst
43 | where r.attribute.Flag == LoaderFlags.OnExit
44 | orderby r.attribute.Level
45 | select (Action)Delegate.CreateDelegate(typeof(Action), r.method);
46 |
47 | var bak = from r in lst
48 | where r.attribute.Flag == LoaderFlags.AsTask
49 | orderby r.attribute.Level
50 | select new { r.path, func = (Func)Delegate.CreateDelegate(typeof(Func), r.method) };
51 |
52 | var run = loa.ToList();
53 | var sav = ext.ToList();
54 | var tsk = bak.ToList();
55 |
56 | lock (_locker)
57 | {
58 | if (_started || _closed)
59 | throw new InvalidOperationException("Framework started or closed!");
60 | _started = true;
61 | _exit = sav;
62 | }
63 |
64 | run.ForEach(r => r.Invoke());
65 | tsk.ForEach(r => Task.Run(() => r.func.Invoke(_cancel.Token)).ContinueWith(t => Log.Info($"Framework task completed, status: {t.Status}, path: {r.path}")));
66 | }
67 |
68 | private void _Close()
69 | {
70 | lock (_locker)
71 | {
72 | if (_closed || _started == false)
73 | return;
74 | _closed = true;
75 | }
76 |
77 | _cancel.Cancel();
78 | _cancel.Dispose();
79 | _exit.ForEach(r => r.Invoke());
80 | }
81 |
82 | public static void Start() => s_ins._Start();
83 |
84 | public static void Close() => s_ins._Close();
85 |
86 | public void Dispose()
87 | {
88 | _cancel.Dispose();
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/code/Messenger/Messenger.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | netcoreapp3.1;net472
6 | Messenger
7 | Messenger
8 | true
9 | true
10 | 8.0
11 | App.ico
12 | App.manifest
13 | Messenger Project
14 | Mikodev
15 | Mikodev 2020
16 | 1.9.0
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/code/Messenger/Models/Host.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace Messenger.Models
4 | {
5 | ///
6 | /// 服务器信息
7 | ///
8 | public class Host
9 | {
10 | ///
11 | /// 协议字符串
12 | ///
13 | public string Protocol { get; set; }
14 |
15 | ///
16 | /// 名称
17 | ///
18 | public string Name { get; set; }
19 |
20 | ///
21 | /// 端口
22 | ///
23 | public int Port { get; set; }
24 |
25 | ///
26 | /// 服务器当前连接的客户端数
27 | ///
28 | public int Count { get; set; }
29 |
30 | ///
31 | /// 服务器最大客户端数
32 | ///
33 | public int CountLimit { get; set; }
34 |
35 | ///
36 | /// 访问延迟 (单次往返耗时 误差较大)
37 | ///
38 | public long Delay { get; set; } = 0;
39 |
40 | ///
41 | /// IP 地址
42 | ///
43 | public IPAddress Address { get; set; } = null;
44 |
45 | ///
46 | /// 依据 IP 地址和端口比较两个对象
47 | ///
48 | public override bool Equals(object obj)
49 | {
50 | if (obj == this)
51 | return true;
52 | var info = obj as Host;
53 | if (info == null)
54 | return false;
55 | if (Port != info.Port)
56 | return false;
57 | if (Address == info.Address)
58 | return true;
59 | if (Address == null || info.Address == null)
60 | return false;
61 | return Address.Equals(info.Address);
62 | }
63 |
64 | ///
65 | /// 调用
66 | ///
67 | public override int GetHashCode()
68 | {
69 | var add = Address;
70 | return add != null
71 | ? new IPEndPoint(add, Port).GetHashCode()
72 | : 0;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/code/Messenger/Models/IFinal.cs:
--------------------------------------------------------------------------------
1 | namespace Messenger.Models
2 | {
3 | internal interface IFinal
4 | {
5 | bool IsFinal { get; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/code/Messenger/Models/LoaderAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Messenger.Models
4 | {
5 | ///
6 | /// 标注有此属性的静态函数将根据指定条件自动执行
7 | ///
8 | [AttributeUsage(AttributeTargets.Method)]
9 | public class LoaderAttribute : Attribute
10 | {
11 | private readonly int _lev;
12 |
13 | private readonly LoaderFlags _tag;
14 |
15 | public int Level => _lev;
16 |
17 | public LoaderFlags Flag => _tag;
18 |
19 | public LoaderAttribute(int level, LoaderFlags flag)
20 | {
21 | _lev = level;
22 | _tag = flag;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/code/Messenger/Models/LoaderFlags.cs:
--------------------------------------------------------------------------------
1 | namespace Messenger.Models
2 | {
3 | public enum LoaderFlags
4 | {
5 | None,
6 |
7 | ///
8 | /// 在程序加载时执行
9 | ///
10 | OnLoad,
11 |
12 | ///
13 | /// 在程序退出时执行
14 | ///
15 | OnExit,
16 |
17 | ///
18 | /// 作为任务在后台运行
19 | ///
20 | AsTask,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/code/Messenger/Models/Packet.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using System;
3 |
4 | namespace Messenger.Models
5 | {
6 | ///
7 | /// 消息记录
8 | ///
9 | public class Packet
10 | {
11 | private readonly string _key;
12 |
13 | private readonly DateTime _timestamp;
14 |
15 | private readonly int _source;
16 |
17 | private readonly int _target;
18 |
19 | private readonly int _index;
20 |
21 | private readonly string _path;
22 |
23 | private readonly object _value;
24 |
25 | private string _image = null;
26 |
27 | private Profile _profile = null;
28 |
29 | public Packet(string key, DateTime datetime, int index, int source, int target, string path, object value)
30 | {
31 | _key = key ?? throw new ArgumentNullException(nameof(key));
32 | _timestamp = datetime;
33 |
34 | _path = path ?? throw new ArgumentNullException(nameof(path));
35 | _value = value;
36 |
37 | _source = source;
38 | _target = target;
39 | _index = index;
40 | }
41 |
42 | public Packet(int index, int source, int target, string path, object value)
43 | {
44 | _path = path ?? throw new ArgumentNullException(nameof(path));
45 | _value = value;
46 |
47 | _source = source;
48 | _target = target;
49 | _index = index;
50 |
51 | _key = Guid.NewGuid().ToString();
52 | _timestamp = DateTime.Now;
53 | }
54 |
55 | public string Key => _key;
56 |
57 | ///
58 | /// 分组索引
59 | ///
60 | public int Index => _index;
61 |
62 | ///
63 | /// 消息时间
64 | ///
65 | public DateTime DateTime => _timestamp;
66 |
67 | ///
68 | /// 收信人编号
69 | ///
70 | public int Target => _target;
71 |
72 | ///
73 | /// 发信人编号
74 | ///
75 | public int Source => _source;
76 |
77 | ///
78 | /// 消息类型
79 | ///
80 | public string Path => _path;
81 |
82 | ///
83 | /// 底层数据 (怎么解读取决于 )
84 | ///
85 | public object Object => _value;
86 |
87 | ///
88 | /// 发送者信息
89 | ///
90 | public Profile Profile
91 | {
92 | get
93 | {
94 | if (_profile == null)
95 | _profile = ProfileModule.Query(_source, true);
96 | return _profile;
97 | }
98 | }
99 |
100 | ///
101 | /// 消息文本
102 | ///
103 | public string MessageText
104 | {
105 | get
106 | {
107 | if (_value is string str && _path == "text")
108 | return str;
109 | return null;
110 | }
111 | }
112 |
113 | ///
114 | /// 图像路径
115 | ///
116 | public string MessageImage
117 | {
118 | get
119 | {
120 | if (_image == null && _value is string str && _path == "image")
121 | _image = CacheModule.GetPath(str);
122 | return _image;
123 | }
124 | }
125 |
126 | ///
127 | /// 提醒
128 | ///
129 | public string MessageNotice
130 | {
131 | get
132 | {
133 | if (_value is string str && _path == "notice")
134 | return str;
135 | return null;
136 | }
137 | }
138 |
139 | public override string ToString()
140 | {
141 | return $"{nameof(Packet)} at {_timestamp:u}, form {_source} to {_target}, path: {_path}, value: {_value}";
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/code/Messenger/Models/Profile.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using Mikodev.Network;
3 | using System.ComponentModel;
4 | using System.Runtime.CompilerServices;
5 |
6 | namespace Messenger.Models
7 | {
8 | ///
9 | /// 用户信息
10 | ///
11 | public class Profile : INotifyPropertyChanging, INotifyPropertyChanged
12 | {
13 | public static event PropertyChangedEventHandler InstancePropertyChanged;
14 |
15 | public static event PropertyChangingEventHandler InstancePropertyChanging;
16 |
17 | public event PropertyChangedEventHandler PropertyChanged;
18 |
19 | public event PropertyChangingEventHandler PropertyChanging;
20 |
21 | private void OnPropertyChange(ref T source, T target, [CallerMemberName] string name = null)
22 | {
23 | var eva = new PropertyChangingEventArgs(name);
24 | PropertyChanging?.Invoke(this, eva);
25 | InstancePropertyChanging?.Invoke(this, eva);
26 |
27 | if (Equals(source, target))
28 | return;
29 | source = target;
30 |
31 | var evb = new PropertyChangedEventArgs(name);
32 | PropertyChanged?.Invoke(this, evb);
33 | InstancePropertyChanged?.Invoke(this, evb);
34 | }
35 |
36 | private readonly int _id;
37 |
38 | private int _hint = 0;
39 |
40 | private string _name = null;
41 |
42 | private string _text = null;
43 |
44 | private string _logo = null;
45 |
46 | private BindingList _messages;
47 |
48 | public Profile(int id) => _id = id;
49 |
50 | public bool IsGroup => _id < Links.Id;
51 |
52 | public bool IsClient => Links.Id < _id;
53 |
54 | public int Id => _id;
55 |
56 | ///
57 | /// 未读消息计数
58 | ///
59 | public int Hint
60 | {
61 | get => _hint;
62 | set => OnPropertyChange(ref _hint, value);
63 | }
64 |
65 | public string Name
66 | {
67 | get => _name;
68 | set => OnPropertyChange(ref _name, value);
69 | }
70 |
71 | public string Text
72 | {
73 | get => _text;
74 | set => OnPropertyChange(ref _text, value);
75 | }
76 |
77 | public string Image
78 | {
79 | get => _logo;
80 | set => OnPropertyChange(ref _logo, value);
81 | }
82 |
83 | ///
84 | /// 获取关联的消息记录列表
85 | ///
86 | public BindingList GetMessages()
87 | {
88 | var lst = _messages;
89 | if (lst != null)
90 | return lst;
91 | lst = HistoryModule.Query(_id);
92 | _messages = lst;
93 | return lst;
94 | }
95 |
96 | ///
97 | /// 获取关联的消息记录列表 (如果尚未创建, 返回 null)
98 | ///
99 | ///
100 | public BindingList GetMessagesOrDefault() => _messages;
101 |
102 | public Profile CopyFrom(Profile profile)
103 | {
104 | Name = profile._name;
105 | Text = profile._text;
106 | Image = profile._logo;
107 | return this;
108 | }
109 |
110 | public override string ToString()
111 | {
112 | return $"{nameof(Profile)} id: {_id}, name: {_name}";
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/code/Messenger/Models/RouteAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Messenger.Models
4 | {
5 | ///
6 | /// 标注有此属性的函数将在收到消息时自动匹配路径执行
7 | ///
8 | [AttributeUsage(AttributeTargets.Method)]
9 | public class RouteAttribute : Attribute
10 | {
11 | private readonly string _pth = null;
12 |
13 | public string Path => _pth;
14 |
15 | public RouteAttribute(string path) => _pth = path;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/code/Messenger/Models/Share.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Net.Sockets;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 |
11 | namespace Messenger.Models
12 | {
13 | public sealed class Share : IFinal, IDisposable, INotifyPropertyChanged
14 | {
15 | internal static Func _backlog;
16 |
17 | internal static void _Register(Share share)
18 | {
19 | _backlog += share._Accept;
20 | }
21 |
22 | ///
23 | /// 通知发送者并返回关联任务
24 | ///
25 | public static async Task Notify(int id, Guid key, Socket socket)
26 | {
27 | var lst = _backlog?.GetInvocationList();
28 | if (lst == null)
29 | return;
30 | foreach (var i in lst)
31 | {
32 | var fun = (Func)i;
33 | var res = fun.Invoke(id, key, socket);
34 | if (res == null)
35 | continue;
36 | await res;
37 | }
38 | }
39 |
40 | internal readonly Guid _key = Guid.NewGuid();
41 |
42 | internal readonly string _name;
43 |
44 | internal readonly string _path;
45 |
46 | internal readonly object _info;
47 |
48 | internal readonly long _length;
49 |
50 | internal readonly BindingList _list = new BindingList();
51 |
52 | internal int _closed = 0;
53 |
54 | #region PropertyChange
55 |
56 | public event PropertyChangedEventHandler PropertyChanged;
57 |
58 | internal void OnPropertyChanged(string str = null) =>
59 | Application.Current.Dispatcher.Invoke(() =>
60 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(str ?? string.Empty)));
61 |
62 | #endregion
63 |
64 | ///
65 | /// 是否为批量操作 (目录: 真, 文件: 假)
66 | ///
67 | public bool IsBatch => _info is DirectoryInfo;
68 |
69 | ///
70 | /// 文件名或目录名
71 | ///
72 | public string Name => _name;
73 |
74 | ///
75 | /// 完整路径
76 | ///
77 | public string Path => _path;
78 |
79 | ///
80 | /// 文件长度
81 | ///
82 | public long Length => _length;
83 |
84 | public BindingList WorkerList => _list;
85 |
86 | public bool IsFinal => Volatile.Read(ref _closed) != 0;
87 |
88 | internal Share(FileSystemInfo info)
89 | {
90 | _info = info;
91 | _name = info.Name;
92 | _path = info.FullName;
93 | }
94 |
95 | public Share(FileInfo info) : this((FileSystemInfo)info)
96 | {
97 | _length = info.Length;
98 | _Register(this);
99 | }
100 |
101 | public Share(DirectoryInfo info) : this((FileSystemInfo)info)
102 | {
103 | _Register(this);
104 | }
105 |
106 | internal Task _Accept(int id, Guid key, Socket socket)
107 | {
108 | if (Volatile.Read(ref _closed) != 0 || key != _key)
109 | return null;
110 | var obj = new ShareWorker(this, id, socket);
111 | Application.Current.Dispatcher.Invoke(() => _list.Add(obj));
112 | return obj.Start();
113 | }
114 |
115 | public void Dispose()
116 | {
117 | if (Interlocked.CompareExchange(ref _closed, 1, 0) != 0)
118 | return;
119 | _backlog -= _Accept;
120 |
121 | OnPropertyChanged(nameof(IsFinal));
122 | var lst = default(List);
123 | _ = Application.Current.Dispatcher.Invoke(() => lst = _list.ToList());
124 | lst.ForEach(r => r.Dispose());
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/code/Messenger/Models/ShareBasic.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Diagnostics;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 |
11 | namespace Messenger.Models
12 | {
13 | public abstract class ShareBasic : IFinal, INotifyPropertyChanged
14 | {
15 | internal class Tick
16 | {
17 | public long Time = 0;
18 |
19 | public long Position = 0;
20 |
21 | public double Speed = 0;
22 | }
23 |
24 | ///
25 | /// 历史记录上限
26 | ///
27 | private const int _tickLimit = 16;
28 |
29 | private const int _delay = 200;
30 |
31 | private static Action s_action = null;
32 |
33 | private static readonly Stopwatch s_watch = new Stopwatch();
34 |
35 | private static readonly Task s_task = new Task(async () =>
36 | {
37 | while (true)
38 | {
39 | s_action?.Invoke();
40 | await Task.Delay(_delay);
41 | }
42 | });
43 |
44 | ///
45 | /// 注册以便实时计算传输进度 (当 为真时自动取消注册)
46 | ///
47 | protected void Register() => s_action += _Refresh;
48 |
49 | #region PropertyChange
50 |
51 | public event PropertyChangedEventHandler PropertyChanged;
52 |
53 | protected void OnPropertyChanged(string str = null) =>
54 | Application.Current.Dispatcher.Invoke(() =>
55 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(str ?? string.Empty)));
56 |
57 | #endregion
58 |
59 | private readonly int _id;
60 |
61 | private int _status = (int)ShareStatus.等待;
62 |
63 | private double _speed = 0;
64 |
65 | private double _progress = 0;
66 |
67 | private TimeSpan _remain = TimeSpan.Zero;
68 |
69 | private readonly List _ticks = new List();
70 |
71 | protected ShareBasic(int id)
72 | {
73 | _id = id;
74 | }
75 |
76 | protected int Id => _id;
77 |
78 | public ShareStatus Status
79 | {
80 | get => (ShareStatus)Volatile.Read(ref _status);
81 | protected set => _SetStatus(value);
82 | }
83 |
84 | public bool IsFinal => (Status & ShareStatus.终止) != 0;
85 |
86 | public abstract long Length { get; }
87 |
88 | public abstract bool IsBatch { get; }
89 |
90 | public abstract string Name { get; }
91 |
92 | public abstract string Path { get; }
93 |
94 | public abstract long Position { get; }
95 |
96 | public Profile Profile => ProfileModule.Query(Id, true);
97 |
98 | public TimeSpan Remain => _remain;
99 |
100 | public double Speed => _speed;
101 |
102 | public double Progress => _progress;
103 |
104 | private void _SetStatus(ShareStatus status)
105 | {
106 | while (true)
107 | {
108 | var cur = Volatile.Read(ref _status);
109 | if ((cur & (int)ShareStatus.终止) != 0)
110 | throw new InvalidOperationException();
111 | if (Interlocked.CompareExchange(ref _status, (int)status, cur) == cur)
112 | break;
113 | continue;
114 | }
115 | }
116 |
117 | private void _Refresh()
118 | {
119 | var fin = IsFinal;
120 |
121 | var avg = _AverageSpeed();
122 | _speed = avg * 1000; // 毫秒 -> 秒
123 | _progress = (Length > 0)
124 | ? (100.0 * Position / Length)
125 | : (fin ? 100 : 0);
126 |
127 | if (IsBatch == false)
128 | {
129 | var spa = (avg > 0 && Position > 0)
130 | ? TimeSpan.FromMilliseconds((Length - Position) / avg)
131 | : TimeSpan.Zero;
132 | // 移除毫秒部分
133 | _remain = new TimeSpan(spa.Days, spa.Hours, spa.Minutes, spa.Seconds);
134 | OnPropertyChanged(nameof(Remain));
135 | }
136 |
137 | OnPropertyChanged(nameof(Speed));
138 | OnPropertyChanged(nameof(Status));
139 | OnPropertyChanged(nameof(Position));
140 | OnPropertyChanged(nameof(Progress));
141 |
142 | // 确保 IsFinal 为真后再计算一次
143 | if (fin == true)
144 | {
145 | s_action -= _Refresh;
146 | OnPropertyChanged(nameof(IsFinal));
147 | }
148 | }
149 |
150 | private double _AverageSpeed()
151 | {
152 | var tic = s_watch.ElapsedMilliseconds;
153 | var cur = new Tick { Time = tic, Position = Position };
154 | if (_ticks.Count > 0)
155 | {
156 | var pre = _ticks[_ticks.Count - 1];
157 | var pos = cur.Position - pre.Position;
158 | var sub = cur.Time - pre.Time;
159 | cur.Speed = 1.0 * pos / sub;
160 | }
161 | _ticks.Add(cur);
162 | // 计算最近几条记录的平均速度
163 | if (_ticks.Count > _tickLimit)
164 | _ticks.RemoveRange(0, _ticks.Count - _tickLimit);
165 | return _ticks.Average(r => r.Speed);
166 | }
167 |
168 | [Loader(16, LoaderFlags.OnLoad)]
169 | public static void Load()
170 | {
171 | s_task.Start();
172 | s_watch.Start();
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/code/Messenger/Models/ShareReceiver.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Modules;
3 | using Mikodev.Binary;
4 | using Mikodev.Logger;
5 | using Mikodev.Network;
6 | using System;
7 | using System.IO;
8 | using System.Net;
9 | using System.Net.Sockets;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace Messenger.Models
14 | {
15 | public sealed class ShareReceiver : ShareBasic, IDisposable
16 | {
17 | private readonly object _locker = new object();
18 |
19 | private readonly CancellationTokenSource _cancel = new CancellationTokenSource();
20 |
21 | internal readonly Guid _key;
22 |
23 | internal readonly long _length;
24 |
25 | internal readonly bool _batch = false;
26 |
27 | ///
28 | /// 原始文件名
29 | ///
30 | internal readonly string _origin;
31 |
32 | internal bool _started = false;
33 |
34 | internal bool _disposed = false;
35 |
36 | internal long _position = 0;
37 |
38 | internal string _name = null;
39 |
40 | internal string _path = null;
41 |
42 | private readonly IPEndPoint[] _endpoints = null;
43 |
44 | public bool IsStarted => _started;
45 |
46 | public bool IsDisposed => _disposed;
47 |
48 | public override long Length => _length;
49 |
50 | public override bool IsBatch => _batch;
51 |
52 | public override string Name => _name;
53 |
54 | public override string Path => _path;
55 |
56 | public override long Position => _position;
57 |
58 | public ShareReceiver(int id, Token reader) : base(id)
59 | {
60 | if (reader == null)
61 | throw new ArgumentNullException(nameof(reader));
62 |
63 | var typ = reader["type"].As();
64 | if (typ == "file")
65 | _length = reader["length"].As();
66 | else if (typ == "dir")
67 | _batch = true;
68 | else
69 | throw new ApplicationException("Invalid share type!");
70 |
71 | _key = reader["key"].As();
72 | _origin = reader["name"].As();
73 | _name = _origin;
74 | _endpoints = reader["endpoints"].As();
75 | }
76 |
77 | public Task Start()
78 | {
79 | lock (_locker)
80 | {
81 | if (_started || _disposed)
82 | throw new InvalidOperationException();
83 | _started = true;
84 | }
85 |
86 | Status = ShareStatus.连接;
87 | Register();
88 | OnPropertyChanged(nameof(IsStarted));
89 | return Task.Run(_Start);
90 | }
91 |
92 | private async Task _Start()
93 | {
94 | var soc = default(Socket);
95 | var iep = default(IPEndPoint);
96 |
97 | for (var i = 0; i < _endpoints.Length; i++)
98 | {
99 | if (soc != null)
100 | break;
101 | soc = new Socket(SocketType.Stream, ProtocolType.Tcp);
102 | iep = _endpoints[i];
103 |
104 | try
105 | {
106 | await soc.ConnectAsyncEx(iep).TimeoutAfter("Share receiver timeout.");
107 | }
108 | catch (Exception err)
109 | {
110 | Log.Error(err);
111 | soc.Dispose();
112 | soc = null;
113 | }
114 | }
115 |
116 | if (soc == null)
117 | {
118 | Status = ShareStatus.失败;
119 | Dispose();
120 | return;
121 | }
122 |
123 | var buf = LinksHelper.Generator.Encode(new
124 | {
125 | path = "share." + (_batch ? "directory" : "file"),
126 | data = _key,
127 | source = LinkModule.Id,
128 | target = Id,
129 | });
130 |
131 | try
132 | {
133 | _ = soc.SetKeepAlive();
134 | await soc.SendAsyncExt(buf);
135 | Status = ShareStatus.运行;
136 | await _Receive(soc, _cancel.Token);
137 | Status = ShareStatus.成功;
138 | PostModule.Notice(Id, _batch ? "share.dir" : "share.file", _origin);
139 | }
140 | catch (OperationCanceledException)
141 | {
142 | Status = ShareStatus.取消;
143 | }
144 | catch (Exception ex)
145 | {
146 | Log.Error(ex);
147 | Status = ShareStatus.中断;
148 | }
149 | finally
150 | {
151 | soc.Dispose();
152 | Dispose();
153 | }
154 | }
155 |
156 | internal Task _Receive(Socket socket, CancellationToken token)
157 | {
158 | void _UpdateInfo(FileSystemInfo info)
159 | {
160 | _name = info.Name;
161 | _path = info.FullName;
162 | OnPropertyChanged(nameof(Name));
163 | OnPropertyChanged(nameof(Path));
164 | }
165 |
166 | if (_batch)
167 | {
168 | var dir = ShareModule.AvailableDirectory(_name);
169 | _UpdateInfo(dir);
170 | return socket.ReceiveDirectoryAsyncEx(dir.FullName, r => _position += r, token);
171 | }
172 | else
173 | {
174 | var inf = ShareModule.AvailableFile(_name);
175 | _UpdateInfo(inf);
176 | return socket.ReceiveFileEx(inf.FullName, _length, r => _position += r, token);
177 | }
178 | }
179 |
180 | public void Dispose()
181 | {
182 | lock (_locker)
183 | {
184 | if (_disposed)
185 | return;
186 | _disposed = true;
187 | }
188 |
189 | _cancel.Cancel();
190 | _cancel.Dispose();
191 | OnPropertyChanged(nameof(IsDisposed));
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/code/Messenger/Models/ShareStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Messenger.Models
2 | {
3 | ///
4 | /// 传输状态
5 | ///
6 | public enum ShareStatus : int
7 | {
8 | 默认 = 0,
9 |
10 | 等待 = 1,
11 |
12 | 连接 = 2,
13 |
14 | 运行 = 4,
15 |
16 | 暂停 = 8,
17 |
18 | 中断 = 16 | 终止,
19 |
20 | 取消 = 32 | 终止,
21 |
22 | 成功 = 64 | 终止,
23 |
24 | 失败 = 128 | 终止,
25 |
26 | 终止 = 1 << 31,
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/code/Messenger/Models/ShareWorker.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Mikodev.Logger;
3 | using System;
4 | using System.IO;
5 | using System.Net.Sockets;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace Messenger.Models
10 | {
11 | public sealed class ShareWorker : ShareBasic, IDisposable
12 | {
13 | internal readonly object _locker = new object();
14 |
15 | internal readonly Share _source;
16 |
17 | internal readonly Socket _socket;
18 |
19 | internal readonly CancellationTokenSource _cancel = new CancellationTokenSource();
20 |
21 | internal long _position = 0;
22 |
23 | internal bool _started = false;
24 |
25 | internal bool _disposed = false;
26 |
27 | public override long Length => _source.Length;
28 |
29 | public override bool IsBatch => _source.IsBatch;
30 |
31 | public override string Name => _source._name;
32 |
33 | public override string Path => _source._path;
34 |
35 | public override long Position => _position;
36 |
37 | public ShareWorker(Share share, int id, Socket socket) : base(id)
38 | {
39 | _source = share;
40 | _socket = socket;
41 | }
42 |
43 | public async Task Start()
44 | {
45 | lock (_locker)
46 | {
47 | if (_started || _disposed)
48 | throw new InvalidOperationException();
49 | _started = true;
50 | }
51 |
52 | Status = ShareStatus.运行;
53 | Register();
54 |
55 | try
56 | {
57 | if (_source._info is FileInfo inf)
58 | await _socket.SendFileEx(_source._path, _source._length, r => _position += r, _cancel.Token);
59 | else
60 | await _socket.SendDirectoryAsyncEx(_source._path, r => _position += r, _cancel.Token);
61 | Status = ShareStatus.成功;
62 | }
63 | catch (OperationCanceledException)
64 | {
65 | Status = ShareStatus.取消;
66 | }
67 | catch (Exception ex)
68 | {
69 | Status = ShareStatus.中断;
70 | Log.Error(ex);
71 | }
72 | finally
73 | {
74 | Dispose();
75 | }
76 | }
77 |
78 | public void Dispose()
79 | {
80 | lock (_locker)
81 | {
82 | if (_disposed)
83 | return;
84 | _disposed = true;
85 | }
86 | _cancel.Cancel();
87 | _cancel.Dispose();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/CacheModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Mikodev.Logger;
3 | using Mikodev.Network;
4 | using System;
5 | using System.Drawing;
6 | using System.Drawing.Imaging;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Security.Cryptography;
10 | using System.Text;
11 |
12 | namespace Messenger.Modules
13 | {
14 | ///
15 | /// 负责管理图片缓存 (被动初始化)
16 | ///
17 | internal class CacheModule
18 | {
19 | private const string _Directory = "Cache";
20 |
21 | private const string _ImageSuffix = ".png";
22 |
23 | private const string _KeyCache = "cache-path";
24 |
25 | ///
26 | /// 图片文件大小限制
27 | ///
28 | private const int _LengthLimit = 4 * 1024 * 1024;
29 |
30 | ///
31 | /// 图像最大分辨率
32 | ///
33 | private const int _PixelLimit = 384;
34 |
35 | ///
36 | /// 图像 DPI
37 | ///
38 | private const float _Density = 96;
39 |
40 | private string _dir = _Directory;
41 |
42 | private static readonly CacheModule s_ins = new CacheModule();
43 |
44 | private CacheModule() { }
45 |
46 | [Loader(16, LoaderFlags.OnLoad)]
47 | public static void Load()
48 | {
49 | try
50 | {
51 | s_ins._dir = EnvironmentModule.Query(_KeyCache, _Directory);
52 | }
53 | catch (Exception ex)
54 | {
55 | Log.Error(ex);
56 | }
57 | }
58 |
59 | ///
60 | /// 计算缓存的 SHA256 值
61 | ///
62 | public static string GetSHA256(byte[] buffer)
63 | {
64 | using (var sha = new SHA256Managed())
65 | {
66 | var buf = sha.ComputeHash(buffer);
67 | var str = buf.Aggregate(new StringBuilder(), (l, r) => l.AppendFormat("{0:x2}", r));
68 | return str.ToString();
69 | }
70 | }
71 |
72 | ///
73 | /// 从本地缓存查找指定 SHA256 值的图像
74 | ///
75 | public static string GetPath(string sha)
76 | {
77 | var dir = new DirectoryInfo(s_ins._dir);
78 | var pth = Path.Combine(dir.FullName, sha + _ImageSuffix);
79 | var inf = new FileInfo(pth);
80 |
81 | try
82 | {
83 | if (inf.Exists == false)
84 | return null;
85 | if (inf.Length < Links.BufferLengthLimit)
86 | return inf.FullName;
87 | Log.Info("Cache file length overflow!");
88 | }
89 | catch (Exception ex)
90 | {
91 | Log.Error(ex);
92 | }
93 | return null;
94 | }
95 |
96 | ///
97 | /// 写入本地缓存, 并将 SHA256 值作为文件名
98 | ///
99 | /// 为真时返回完整路径, 否则返回 SHA256 值
100 | public static string SetBuffer(byte[] buffer, bool fullPath, bool nothrow = true)
101 | {
102 | if (buffer.Length > Links.BufferLengthLimit)
103 | {
104 | Log.Info("Cache buffer length overflow!");
105 | return null;
106 | }
107 |
108 | var fst = default(FileStream);
109 | var sha = GetSHA256(buffer);
110 | var pth = default(string);
111 |
112 | try
113 | {
114 | var dir = new DirectoryInfo(s_ins._dir);
115 | if (dir.Exists == false)
116 | dir.Create();
117 | pth = Path.Combine(dir.FullName, sha + _ImageSuffix);
118 | if (File.Exists(pth) == false)
119 | {
120 | fst = new FileStream(pth, FileMode.CreateNew, FileAccess.Write);
121 | fst.Write(buffer, 0, buffer.Length);
122 | }
123 | }
124 | catch (Exception ex)
125 | {
126 | if (nothrow == false)
127 | throw;
128 | Log.Error(ex);
129 | return null;
130 | }
131 | finally
132 | {
133 | fst?.Dispose();
134 | }
135 | return fullPath ? pth : sha;
136 | }
137 |
138 | ///
139 | /// 从图像中裁剪出正方形区域 (用于个人头像)
140 | ///
141 | public static byte[] ImageSquare(string filepath)
142 | {
143 | var inf = new FileInfo(filepath);
144 | if (inf.Length > _LengthLimit)
145 | throw new IOException("File too big!");
146 | var bmp = new Bitmap(filepath);
147 | var src = new Rectangle();
148 | if (bmp.Width > bmp.Height)
149 | src = new Rectangle((bmp.Width - bmp.Height) / 2, 0, bmp.Height, bmp.Height);
150 | else
151 | src = new Rectangle(0, (bmp.Height - bmp.Width) / 2, bmp.Width, bmp.Width);
152 | var len = bmp.Width > bmp.Height ? bmp.Height : bmp.Width;
153 | var div = 1;
154 | for (div = 1; len / div > _PixelLimit; div++) ;
155 | var dst = new Rectangle(0, 0, len / div, len / div);
156 | return _LoadImage(bmp, src, dst, ImageFormat.Jpeg);
157 | }
158 |
159 | ///
160 | /// 按比例缩放图像 (用于聊天)
161 | ///
162 | public static byte[] ImageZoom(string filepath)
163 | {
164 | var inf = new FileInfo(filepath);
165 | if (inf.Length > _LengthLimit)
166 | throw new IOException("File too big!");
167 | var bmp = new Bitmap(filepath);
168 | var len = bmp.Size;
169 | var div = 1;
170 | for (div = 1; len.Width / div > _PixelLimit || len.Height / div > _PixelLimit; div++) ;
171 |
172 | var src = new Rectangle(0, 0, bmp.Width, bmp.Height);
173 | var dst = new Rectangle(0, 0, len.Width / div, len.Height / div);
174 |
175 | return _LoadImage(bmp, src, dst, ImageFormat.Png);
176 | }
177 |
178 | private static byte[] _LoadImage(Bitmap bmp, Rectangle src, Rectangle dst, ImageFormat format)
179 | {
180 | var img = new Bitmap(dst.Right, dst.Bottom);
181 | var gra = Graphics.FromImage(img);
182 | var mst = new MemoryStream();
183 | var buf = default(byte[]);
184 |
185 | try
186 | {
187 | img.SetResolution(_Density, _Density);
188 | if (format != ImageFormat.Png)
189 | gra.Clear(Color.Black);
190 | gra.DrawImage(bmp, dst, src, GraphicsUnit.Pixel);
191 | img.Save(mst, format);
192 | buf = mst.ToArray();
193 | }
194 | finally
195 | {
196 | mst?.Dispose();
197 | gra?.Dispose();
198 | bmp?.Dispose();
199 | img?.Dispose();
200 | }
201 | return buf;
202 | }
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/EnvironmentModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Mikodev.Logger;
3 | using Mikodev.Network;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using System.Xml;
12 | using static Messenger.Extensions.Extension;
13 |
14 | namespace Messenger.Modules
15 | {
16 | ///
17 | /// 管理用户设置
18 | ///
19 | internal class EnvironmentModule
20 | {
21 | private const int _NoticeDelay = 1000;
22 |
23 | private const int _NoticeErrorDelay = 15 * 1000 - _NoticeDelay;
24 |
25 | private static readonly TimeSpan _NoticeInterval = TimeSpan.FromSeconds(10);
26 |
27 | private const string _Path = nameof(Messenger) + ".settings.xml";
28 |
29 | private const string _Root = "settings";
30 |
31 | private const string _Header = "setting";
32 |
33 | private const string _Key = "key";
34 |
35 | private const string _Value = "value";
36 |
37 | private readonly object _locker = new object();
38 |
39 | private readonly Dictionary _settings = new Dictionary();
40 |
41 | private readonly LinkNoticeSource _source = new LinkNoticeSource(_NoticeInterval);
42 |
43 | private static readonly EnvironmentModule s_ins = new EnvironmentModule();
44 |
45 | private EnvironmentModule() { }
46 |
47 | private void _Load(XmlDocument document)
48 | {
49 | var itm = document.SelectNodes($"/{_Root}/{_Header}[@{_Key}]");
50 | foreach (var i in itm)
51 | {
52 | var ele = (XmlElement)i;
53 | var key = (XmlAttribute)ele.SelectSingleNode($"@{_Key}");
54 | // Maybe null
55 | var val = (XmlAttribute)ele.SelectSingleNode($"@{_Value}");
56 | _Update(key.Value, val?.Value);
57 | }
58 | }
59 |
60 | [Loader(0, LoaderFlags.OnLoad)]
61 | public static void Load()
62 | {
63 | var fst = default(FileStream);
64 | var doc = default(XmlDocument);
65 |
66 | try
67 | {
68 | var inf = new FileInfo(_Path);
69 | if (inf.Exists == false)
70 | return;
71 | fst = new FileStream(_Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
72 | if (fst.Length > Links.BufferLengthLimit)
73 | return;
74 | doc = new XmlDocument();
75 | doc.Load(fst);
76 | }
77 | catch (Exception ex)
78 | {
79 | Log.Error(ex);
80 | }
81 | finally
82 | {
83 | fst?.Dispose();
84 | }
85 |
86 | // Do not call this method if xml file not disposed!!!
87 | s_ins._Load(doc);
88 | }
89 |
90 | ///
91 | /// 保存配置并忽略异常
92 | ///
93 | private bool _Save(string path)
94 | {
95 | var lst = Lock(_locker, () => _settings.ToList());
96 | var doc = new XmlDocument();
97 | var top = doc.CreateElement(_Root);
98 | _ = doc.AppendChild(top);
99 | lst.Sort((a, b) => a.Key.CompareTo(b.Key));
100 | foreach (var i in lst)
101 | {
102 | var key = i.Key;
103 | var val = i.Value;
104 | var ele = doc.CreateElement(_Header);
105 | ele.SetAttribute(_Key, key);
106 | if (val != null)
107 | ele.SetAttribute(_Value, val);
108 | _ = top.AppendChild(ele);
109 | }
110 |
111 | var fst = default(FileStream);
112 | var wtr = default(StreamWriter);
113 | var res = false;
114 |
115 | try
116 | {
117 | fst = new FileStream(path, FileMode.Create);
118 | wtr = new StreamWriter(fst, Encoding.UTF8);
119 | doc.Save(wtr);
120 | res = true;
121 | }
122 | catch (Exception ex)
123 | {
124 | Log.Error(ex);
125 | }
126 | finally
127 | {
128 | wtr?.Dispose();
129 | fst?.Dispose();
130 | }
131 | return res;
132 | }
133 |
134 | private void _Exit()
135 | {
136 | var res = _source.GetNotice(true);
137 | if (res.IsAny == false)
138 | return;
139 | _ = _Save(_Path);
140 | res.Handled();
141 | }
142 |
143 | private string _Query(string key, string value)
144 | {
145 | lock (_locker)
146 | {
147 | if (_settings.TryGetValue(key, out var val))
148 | return val;
149 | _settings.Add(key, value);
150 | }
151 | _source.Update();
152 | return value;
153 | }
154 |
155 | private void _Update(string key, string value)
156 | {
157 | lock (_locker)
158 | {
159 | if (_settings.TryGetValue(key, out var val) && Equals(val, value))
160 | return;
161 | _settings[key] = value;
162 | }
163 | _source.Update();
164 | }
165 |
166 | public static string Query(string key, string defaultValue = null) => s_ins._Query(key, defaultValue);
167 |
168 | public static void Update(string key, string value) => s_ins._Update(key, value);
169 |
170 | [Loader(int.MaxValue, LoaderFlags.OnExit)]
171 | public static void Exit() => s_ins._Exit();
172 |
173 | [Loader(0, LoaderFlags.AsTask)]
174 | public static async Task Scan(CancellationToken token)
175 | {
176 | while (true)
177 | {
178 | token.ThrowIfCancellationRequested();
179 | await Task.Delay(_NoticeDelay);
180 | token.ThrowIfCancellationRequested();
181 |
182 | var res = s_ins._source.Notice();
183 | if (res.IsAny == false)
184 | continue;
185 |
186 | if (s_ins._Save(_Path) == false)
187 | await Task.Delay(_NoticeErrorDelay);
188 | else
189 | res.Handled();
190 | continue;
191 | }
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/HostModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Models;
3 | using Mikodev.Binary;
4 | using Mikodev.Logger;
5 | using Mikodev.Network;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Diagnostics;
9 | using System.Linq;
10 | using System.Net;
11 | using System.Net.Sockets;
12 | using System.Threading.Tasks;
13 |
14 | namespace Messenger.Modules
15 | {
16 | ///
17 | /// 搜索和管理服务器信息
18 | ///
19 | internal class HostModule
20 | {
21 | private const int _Timeout = 1000;
22 |
23 | private const string _KeyLast = "server-last";
24 |
25 | private const string _KeyList = "server-broadcast-list";
26 |
27 | private string _host = null;
28 |
29 | private int _port = 0;
30 |
31 | private readonly List _points = new List();
32 |
33 | private static readonly HostModule s_ins = new HostModule();
34 |
35 | public static string Name
36 | {
37 | get => s_ins._host;
38 | set
39 | {
40 | s_ins._host = value;
41 | EnvironmentModule.Update(_KeyLast, $"{value}:{s_ins._port}");
42 | }
43 | }
44 |
45 | public static int Port
46 | {
47 | get => s_ins._port;
48 | set
49 | {
50 | s_ins._port = value;
51 | EnvironmentModule.Update(_KeyLast, $"{s_ins._host}:{value}");
52 | }
53 | }
54 |
55 | internal static Host GetHostInfo(byte[] buffer, int offset, int length)
56 | {
57 | try
58 | {
59 | var rea = new Token(LinksHelper.Generator, new ReadOnlyMemory(buffer, offset, length));
60 | var inf = new Host()
61 | {
62 | Protocol = rea["protocol"].As(),
63 | Port = rea["port"].As(),
64 | Name = rea["name"].As(),
65 | Count = rea["count"].As(),
66 | CountLimit = rea["limit"].As(),
67 | };
68 | return inf;
69 | }
70 | catch (Exception ex)
71 | {
72 | Log.Error(ex);
73 | return null;
74 | }
75 | }
76 |
77 | ///
78 | /// 通过 UDP 广播从搜索列表搜索服务器
79 | ///
80 | ///
81 | public static async Task Refresh()
82 | {
83 | var lst = new List();
84 | var soc = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
85 | var txt = LinksHelper.Generator.Encode(new { protocol = Links.Protocol });
86 | var mis = new List();
87 |
88 | async Task _RefreshAsync()
89 | {
90 | var buf = new byte[Links.BufferLength];
91 | var stw = Stopwatch.StartNew();
92 | while (stw.ElapsedMilliseconds < _Timeout)
93 | {
94 | var len = soc.Available;
95 | if (len < 1)
96 | {
97 | if (stw.ElapsedMilliseconds > _Timeout)
98 | break;
99 | await Task.Delay(4);
100 | continue;
101 | }
102 |
103 | var iep = new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort) as EndPoint;
104 | len = Math.Min(len, buf.Length);
105 | len = soc.ReceiveFrom(buf, 0, len, SocketFlags.None, ref iep);
106 | var inf = GetHostInfo(buf, 0, len);
107 | if (inf == null || inf.Protocol != Links.Protocol)
108 | continue;
109 | inf.Address = ((IPEndPoint)iep).Address;
110 | inf.Delay = stw.ElapsedMilliseconds;
111 |
112 | if (lst.Find(r => r.Equals(inf)) != null)
113 | continue;
114 | lst.Add(inf);
115 | }
116 | }
117 |
118 | try
119 | {
120 | soc.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
121 | soc.Bind(new IPEndPoint(IPAddress.Any, 0));
122 |
123 | var run = Task.Run(_RefreshAsync);
124 | foreach (var a in s_ins._points)
125 | _ = soc.SendTo(txt, a);
126 | await run;
127 | }
128 | catch (Exception ex) when (ex is SocketException || ex is AggregateException)
129 | {
130 | Log.Error(ex);
131 | }
132 | finally
133 | {
134 | soc.Dispose();
135 | }
136 |
137 | return lst.ToArray();
138 | }
139 |
140 | ///
141 | /// 读取服务器搜索列表
142 | ///
143 | [Loader(4, LoaderFlags.OnLoad)]
144 | public static void Load()
145 | {
146 | var lst = new List();
147 | var hos = default(string);
148 | var pot = Links.BroadcastPort;
149 | var iep = new IPEndPoint(IPAddress.Broadcast, Links.BroadcastPort);
150 |
151 | try
152 | {
153 | var sts = EnvironmentModule.Query(_KeyList, iep.ToString());
154 | foreach (var s in sts.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
155 | lst.Add(s.ToEndPointEx());
156 |
157 | var str = EnvironmentModule.Query(_KeyLast, $"{IPAddress.Loopback}:{Links.Port}");
158 | _ = Extension.ToHostEx(str, out hos, out pot);
159 | }
160 | catch (Exception ex)
161 | {
162 | Log.Error(ex);
163 | }
164 |
165 | if (lst.Count < 1)
166 | lst.Add(iep);
167 |
168 | var res = s_ins._points;
169 | res.Clear();
170 | foreach (var i in lst.Distinct())
171 | res.Add(i);
172 |
173 | s_ins._host = hos;
174 | s_ins._port = pot;
175 | return;
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/LinkModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Models;
3 | using Mikodev.Logger;
4 | using Mikodev.Network;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.ComponentModel;
8 | using System.Linq;
9 | using System.Net;
10 | using System.Net.Sockets;
11 | using System.Threading.Tasks;
12 | using System.Windows;
13 | using System.Windows.Interop;
14 |
15 | namespace Messenger.Modules
16 | {
17 | ///
18 | /// 维持客户端与服务器的连接, 并负责引发事件
19 | ///
20 | internal class LinkModule
21 | {
22 | private LinkModule() { }
23 |
24 | private static readonly LinkModule s_ins = new LinkModule();
25 |
26 | private LinkClient _client = null;
27 |
28 | private readonly object _locker = new object();
29 |
30 | public static int Id => s_ins._client?.Id ?? ProfileModule.Id;
31 |
32 | public static bool IsRunning => s_ins._client?.IsRunning ?? false;
33 |
34 | ///
35 | /// 启动连接 (与 方法为非完全线程安全的关系, 不过两个方法不可能同时调用)
36 | ///
37 | public static async Task Start(int id, IPEndPoint endpoint)
38 | {
39 | var clt = await LinkClient.Connect(id, endpoint, _RequestHandler);
40 |
41 | void _OnReceived(object sender, LinkEventArgs args) => RouteModule.Invoke(args.Object);
42 |
43 | void _OnDisposed(object sender, LinkEventArgs args)
44 | {
45 | clt.Received -= _OnReceived;
46 | clt.Disposed -= _OnDisposed;
47 |
48 | // 置空
49 | lock (s_ins._locker)
50 | s_ins._client = null;
51 | var obj = args.Object;
52 | if (obj == null || obj is OperationCanceledException)
53 | return;
54 | Entrance.ShowError("连接中断", obj);
55 | }
56 |
57 | clt.Received += _OnReceived;
58 | clt.Disposed += _OnDisposed;
59 |
60 | lock (s_ins._locker)
61 | {
62 | if (s_ins._client != null)
63 | {
64 | clt.Dispose();
65 | throw new InvalidOperationException();
66 | }
67 | s_ins._client = clt;
68 | }
69 |
70 | HistoryModule.Handled += _HistoryHandled;
71 | ShareModule.PendingList.ListChanged += _PendingListChanged;
72 |
73 | ProfileModule.SetId(id);
74 |
75 | PostModule.UserProfile(Links.Id);
76 | PostModule.UserRequest();
77 | PostModule.UserGroups();
78 |
79 | return clt.Start();
80 | }
81 |
82 | private static async Task _RequestHandler(Socket socket, LinkPacket packet)
83 | {
84 | var pth = packet.Path;
85 | if (pth == "share.directory" || pth == "share.file")
86 | {
87 | var src = packet.Source;
88 | var key = packet.Data.As();
89 | await Share.Notify(src, key, socket);
90 | }
91 | else
92 | {
93 | Log.Info($"Path \"{pth}\" not supported.");
94 | }
95 | }
96 |
97 | [Loader(0, LoaderFlags.OnExit)]
98 | public static void Shutdown()
99 | {
100 | lock (s_ins._locker)
101 | {
102 | s_ins._client?.Dispose();
103 | s_ins._client = null;
104 | }
105 |
106 | ShareModule.Shutdown();
107 | ProfileModule.Clear();
108 | HistoryModule.Handled -= _HistoryHandled;
109 | ShareModule.PendingList.ListChanged -= _PendingListChanged;
110 | }
111 |
112 | public static void Enqueue(byte[] buffer) => s_ins._client?.Enqueue(buffer);
113 |
114 | ///
115 | /// 获取与连接关联的 NAT 内部端点和外部端点 (二者相同时只返回一个, 连接无效时返回空列表, 始终不会返回 null)
116 | ///
117 | public static List GetEndPoints()
118 | {
119 | var lst = new List();
120 | if (Extension.Lock(s_ins._locker, ref s_ins._client, out var clt) == false)
121 | return lst;
122 | lst.Add(clt.InnerEndPoint);
123 | lst.Add(clt.OuterEndPoint);
124 | var res = lst.Distinct().ToList();
125 | return res;
126 | }
127 |
128 | private static void _HistoryHandled(object sender, LinkEventArgs e)
129 | {
130 | var hdl = new WindowInteropHelper(Application.Current.MainWindow).Handle;
131 | if (e.Finish == false || Application.Current.MainWindow.IsActive == false)
132 | _ = NativeMethods.FlashWindow(hdl, true);
133 | return;
134 | }
135 |
136 | private static void _PendingListChanged(object sender, ListChangedEventArgs e)
137 | {
138 | if (sender == ShareModule.PendingList && e.ListChangedType == ListChangedType.ItemAdded)
139 | {
140 | if (Application.Current.MainWindow.IsActive == true)
141 | return;
142 | var hdl = new WindowInteropHelper(Application.Current.MainWindow).Handle;
143 | _ = NativeMethods.FlashWindow(hdl, true);
144 | }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/PostModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Mikodev.Binary;
3 | using Mikodev.Network;
4 | using System.IO;
5 | using System.Windows;
6 |
7 | namespace Messenger.Modules
8 | {
9 | internal class PostModule
10 | {
11 | public static void Text(int dst, string val)
12 | {
13 | var buf = LinksHelper.Generator.Encode(new
14 | {
15 | source = LinkModule.Id,
16 | target = dst,
17 | path = "msg.text",
18 | data = val,
19 | });
20 | LinkModule.Enqueue(buf);
21 | _ = HistoryModule.Insert(dst, "text", val);
22 | }
23 |
24 | public static void Image(int dst, byte[] val)
25 | {
26 | var buf = LinksHelper.Generator.Encode(new
27 | {
28 | source = LinkModule.Id,
29 | target = dst,
30 | path = "msg.image",
31 | data = val,
32 | });
33 | LinkModule.Enqueue(buf);
34 | _ = HistoryModule.Insert(dst, "image", val);
35 | }
36 |
37 | ///
38 | /// Post feedback message
39 | ///
40 | public static void Notice(int dst, string genre, string arg)
41 | {
42 | var buf = LinksHelper.Generator.Encode(new
43 | {
44 | source = LinkModule.Id,
45 | target = dst,
46 | path = "msg.notice",
47 | data = new
48 | {
49 | type = genre,
50 | parameter = arg,
51 | },
52 | });
53 | LinkModule.Enqueue(buf);
54 | // you don't have to notice yourself in history module
55 | }
56 |
57 | ///
58 | /// 向指定用户发送本机用户信息
59 | ///
60 | public static void UserProfile(int dst)
61 | {
62 | var pro = ProfileModule.Current;
63 | var buf = LinksHelper.Generator.Encode(new
64 | {
65 | source = LinkModule.Id,
66 | target = dst,
67 | path = "user.profile",
68 | data = new
69 | {
70 | id = ProfileModule.Id,
71 | name = pro.Name,
72 | text = pro.Text,
73 | image = ProfileModule.ImageBuffer,
74 | },
75 | });
76 | LinkModule.Enqueue(buf);
77 | }
78 |
79 | public static void UserRequest()
80 | {
81 | var buf = LinksHelper.Generator.Encode(new
82 | {
83 | source = LinkModule.Id,
84 | target = Links.Id,
85 | path = "user.request",
86 | });
87 | LinkModule.Enqueue(buf);
88 | }
89 |
90 | ///
91 | /// 发送请求监听的用户组
92 | ///
93 | public static void UserGroups()
94 | {
95 | var buf = LinksHelper.Generator.Encode(new
96 | {
97 | source = LinkModule.Id,
98 | target = Links.Id,
99 | path = "user.group",
100 | data = ProfileModule.GroupIds,
101 | });
102 | LinkModule.Enqueue(buf);
103 | }
104 |
105 | ///
106 | /// 发送文件信息
107 | ///
108 | public static void File(int dst, string filepath)
109 | {
110 | var sha = new Share(new FileInfo(filepath));
111 | Application.Current.Dispatcher.Invoke(() => ShareModule.ShareList.Add(sha));
112 | var buf = LinksHelper.Generator.Encode(new
113 | {
114 | source = LinkModule.Id,
115 | target = dst,
116 | path = "share.info",
117 | data = new
118 | {
119 | key = sha._key,
120 | type = "file",
121 | name = sha.Name,
122 | length = sha.Length,
123 | endpoints = LinkModule.GetEndPoints(),
124 | }
125 | });
126 | LinkModule.Enqueue(buf);
127 | _ = HistoryModule.Insert(dst, "share", sha);
128 | }
129 |
130 | public static void Directory(int dst, string directory)
131 | {
132 | var sha = new Share(new DirectoryInfo(directory));
133 | Application.Current.Dispatcher.Invoke(() => ShareModule.ShareList.Add(sha));
134 | var buf = LinksHelper.Generator.Encode(new
135 | {
136 | source = LinkModule.Id,
137 | target = dst,
138 | path = "share.info",
139 | data = new
140 | {
141 | key = sha._key,
142 | type = "dir",
143 | name = sha.Name,
144 | endpoints = LinkModule.GetEndPoints(),
145 | }
146 | });
147 | LinkModule.Enqueue(buf);
148 | _ = HistoryModule.Insert(dst, "share", sha);
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/RouteModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using Messenger.Models;
3 | using Mikodev.Logger;
4 | using Mikodev.Network;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Linq.Expressions;
9 |
10 | namespace Messenger.Modules
11 | {
12 | ///
13 | /// 处理消息, 并分发给各个消息处理函数
14 | ///
15 | internal class RouteModule
16 | {
17 | private static readonly RouteModule s_ins = new RouteModule();
18 |
19 | private readonly Dictionary> _dic = new Dictionary>();
20 |
21 | private RouteModule() { }
22 |
23 | private void _Load()
24 | {
25 | /* 利用反射识别所有控制器
26 | * 同时构建表达式以便提升运行速度 */
27 | var fun = typeof(LinkExtension).GetMethods().First(r => r.Name == nameof(LinkExtension.LoadValue));
28 | var lst = Extension.FindAttribute(
29 | typeof(RouteAttribute).Assembly, typeof(RouteAttribute),
30 | typeof(LinkPacket),
31 | (a, m, t) => new { Attribute = (RouteAttribute)a, MethodInfo = m, Type = t }
32 | ).ToList();
33 |
34 | var res = lst.Select(i =>
35 | {
36 | var buf = Expression.Parameter(typeof(byte[]), "buffer");
37 | var val = Expression.Call(fun, Expression.New(i.Type), buf);
38 | var cvt = Expression.Convert(val, i.Type);
39 | var act = Expression.Lambda>(Expression.Call(cvt, i.MethodInfo), buf);
40 | return new { Path = i.Attribute.Path, Action = act.Compile() };
41 | });
42 |
43 | foreach (var i in res)
44 | _dic.Add(i.Path, i.Action);
45 | return;
46 | }
47 |
48 | public static void Invoke(LinkPacket arg)
49 | {
50 | var dic = s_ins._dic;
51 | if (dic.TryGetValue(arg.Path, out var act))
52 | act.Invoke(arg.Buffer);
53 | else
54 | Log.Info($"Path \"{arg.Path}\" not supported.");
55 | return;
56 | }
57 |
58 | [Loader(1, LoaderFlags.OnLoad)]
59 | public static void Load()
60 | {
61 | s_ins._Load();
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/SettingModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using System.ComponentModel;
3 |
4 | namespace Messenger.Modules
5 | {
6 | ///
7 | /// 管理用户界面设置
8 | ///
9 | internal class SettingModule : INotifyPropertyChanging, INotifyPropertyChanged
10 | {
11 | private const string _KeyCtrlEnter = "hotkey-control-enter";
12 |
13 | private SettingModule() { }
14 |
15 | private static readonly SettingModule s_ins = new SettingModule();
16 |
17 | public static SettingModule Instance => s_ins;
18 |
19 | private bool _ctrlenter = false;
20 |
21 | public event PropertyChangingEventHandler PropertyChanging;
22 |
23 | public event PropertyChangedEventHandler PropertyChanged;
24 |
25 | public bool UseControlEnter
26 | {
27 | get => _ctrlenter;
28 | set
29 | {
30 | var changing = PropertyChanging;
31 | if (changing != null)
32 | {
33 | changing.Invoke(this, new PropertyChangingEventArgs(nameof(UseEnter)));
34 | changing.Invoke(this, new PropertyChangingEventArgs(nameof(UseControlEnter)));
35 | }
36 |
37 | if (_ctrlenter == value)
38 | return;
39 | _ctrlenter = value;
40 | EnvironmentModule.Update(_KeyCtrlEnter, value.ToString());
41 |
42 | var changed = PropertyChanged;
43 | if (changed != null)
44 | {
45 | changed.Invoke(this, new PropertyChangedEventArgs(nameof(UseEnter)));
46 | changed.Invoke(this, new PropertyChangedEventArgs(nameof(UseControlEnter)));
47 | }
48 | }
49 | }
50 |
51 | public bool UseEnter
52 | {
53 | get => UseControlEnter == false;
54 | set => UseControlEnter = (value == false);
55 | }
56 |
57 | [Loader(8, LoaderFlags.OnLoad)]
58 | public static void Load()
59 | {
60 | var str = EnvironmentModule.Query(_KeyCtrlEnter, false.ToString());
61 | if (str != null && bool.TryParse(str, out var res))
62 | s_ins._ctrlenter = res;
63 | return;
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/code/Messenger/Modules/ShareModule.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.CompilerServices;
8 | using System.Windows;
9 |
10 | namespace Messenger.Modules
11 | {
12 | ///
13 | /// 管理共享并提供界面绑定功能
14 | ///
15 | internal class ShareModule : INotifyPropertyChanging, INotifyPropertyChanged
16 | {
17 | private const string _KeyPath = "share-save-path";
18 |
19 | private const string _Path = "Share";
20 |
21 | private bool _hasShare = false;
22 |
23 | private bool _hasReceiver = false;
24 |
25 | private bool _hasPending = false;
26 |
27 | private string _savepath = null;
28 |
29 | private readonly BindingList _shareList = new BindingList();
30 |
31 | private readonly BindingList _receiverList = new BindingList();
32 |
33 | private readonly BindingList _pendingList = new BindingList();
34 |
35 | public bool HasShare
36 | {
37 | get => _hasShare;
38 | set => OnPropertyChange(ref _hasShare, value);
39 | }
40 |
41 | public bool HasReceiver
42 | {
43 | get => _hasReceiver;
44 | set => OnPropertyChange(ref _hasReceiver, value);
45 | }
46 |
47 | public bool HasPending
48 | {
49 | get => _hasPending;
50 | set => OnPropertyChange(ref _hasPending, value);
51 | }
52 |
53 | #region PropertyChange
54 |
55 | public event PropertyChangedEventHandler PropertyChanged;
56 |
57 | public event PropertyChangingEventHandler PropertyChanging;
58 |
59 | private void OnPropertyChange(ref T source, T target, [CallerMemberName] string name = null)
60 | {
61 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(name));
62 | if (Equals(source, target))
63 | return;
64 | source = target;
65 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
66 | }
67 |
68 | #endregion
69 |
70 | private ShareModule()
71 | {
72 | _shareList.ListChanged += (s, e) => HasShare = _shareList.Count > 0;
73 | _receiverList.ListChanged += (s, e) => HasReceiver = _receiverList.Count > 0;
74 | _pendingList.ListChanged += (s, e) => HasPending = _pendingList.Count > 0;
75 | }
76 |
77 | // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
78 |
79 | private static readonly ShareModule s_ins = new ShareModule();
80 |
81 | public static string SavePath
82 | {
83 | get => s_ins._savepath;
84 | set
85 | {
86 | s_ins._savepath = value;
87 | EnvironmentModule.Update(_KeyPath, value);
88 | }
89 | }
90 |
91 | public static ShareModule Instance => s_ins;
92 |
93 | public static BindingList ShareList => s_ins._shareList;
94 |
95 | public static BindingList ReceiverList => s_ins._receiverList;
96 |
97 | public static BindingList PendingList => s_ins._pendingList;
98 |
99 | ///
100 | /// 注册一个接收器并添加到待办列表 (在其启动或关闭后自动从待办列表中移除)
101 | ///
102 | public static void Register(ShareReceiver receiver)
103 | {
104 | Application.Current.Dispatcher.Invoke(() =>
105 | {
106 | s_ins._receiverList.Add(receiver);
107 | s_ins._pendingList.Add(receiver);
108 | receiver.PropertyChanged += _RemovePending;
109 | });
110 | }
111 |
112 | private static void _RemovePending(object sender, PropertyChangedEventArgs e)
113 | {
114 | var pro = e.PropertyName;
115 | if (pro != nameof(ShareReceiver.IsStarted) && pro != nameof(ShareReceiver.IsDisposed))
116 | return;
117 | var obj = s_ins._pendingList.FirstOrDefault(r => ReferenceEquals(r, sender));
118 | if (obj == null)
119 | return;
120 | _ = s_ins._pendingList.Remove(obj);
121 | obj.PropertyChanged -= _RemovePending;
122 | }
123 |
124 | ///
125 | /// 取消所有共享任务
126 | ///
127 | public static void Shutdown()
128 | {
129 | Application.Current.Dispatcher.Invoke(() =>
130 | {
131 | foreach (var i in s_ins._shareList)
132 | i.Dispose();
133 | foreach (var i in s_ins._receiverList)
134 | i.Dispose();
135 | });
136 | }
137 |
138 | ///
139 | /// 移除所有 值为真的项目, 返回被移除的项目
140 | ///
141 | public static List Remove()
142 | {
143 | var lst = new List();
144 | void _Remove(IList list) where T : IFinal
145 | {
146 | for (var i = 0; i < list.Count; i++)
147 | {
148 | var val = list[i];
149 | if (val.IsFinal == false)
150 | continue;
151 | lst.Add(val);
152 | list.RemoveAt(i);
153 | i--;
154 | }
155 | }
156 |
157 | Application.Current.Dispatcher.Invoke(() =>
158 | {
159 | foreach (var i in s_ins._shareList)
160 | _Remove(i.WorkerList);
161 | _Remove(s_ins._shareList);
162 | _Remove(s_ins._receiverList);
163 | });
164 | return lst;
165 | }
166 |
167 | #region Other methods
168 |
169 | ///
170 | /// 检查文件名在指定目录下是否可用 如果冲突则添加随机后缀并重试 再次失败则抛出异常
171 | ///
172 | /// 文件名
173 | ///
174 | public static FileInfo AvailableFile(string name)
175 | {
176 | var dir = new DirectoryInfo(s_ins._savepath);
177 | if (dir.Exists == false)
178 | dir.Create();
179 | var inf = new FileInfo(Path.Combine(dir.FullName, name));
180 | if (inf.Exists == false)
181 | return inf;
182 |
183 | var pre = Path.GetFileNameWithoutExtension(name);
184 | var ext = Path.GetExtension(name);
185 | var str = $"{pre}@{DateTime.Now:yyyyMMdd-HHmmss-fff}{ext}";
186 | var res = new FileInfo(Path.Combine(dir.FullName, str));
187 | if (res.Exists)
188 | throw new IOException();
189 | return res;
190 | }
191 |
192 | ///
193 | /// 检查目录名在指定目录下是否可用 如果冲突则添加随机后缀并重试 再次失败则抛出异常
194 | ///
195 | /// 目录名
196 | ///
197 | public static DirectoryInfo AvailableDirectory(string name)
198 | {
199 | var dir = new DirectoryInfo(s_ins._savepath);
200 | if (dir.Exists == false)
201 | dir.Create();
202 | var pth = Path.Combine(dir.FullName, name);
203 | var inf = new DirectoryInfo(pth);
204 | if (inf.Exists == false)
205 | return inf;
206 |
207 | var str = $"{name}@{DateTime.Now:yyyyMMdd-HHmmss-fff}";
208 | var res = new DirectoryInfo(Path.Combine(dir.FullName, str));
209 | if (res.Exists)
210 | throw new IOException();
211 | return res;
212 | }
213 |
214 | [Loader(32, LoaderFlags.OnLoad)]
215 | public static void Load()
216 | {
217 | s_ins._savepath = EnvironmentModule.Query(_KeyPath, _Path);
218 | }
219 |
220 | #endregion
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/code/Messenger/PageClient.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 用户
23 | 所有在线的用户
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/code/Messenger/PageClient.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger
6 | {
7 | ///
8 | /// PageClient.xaml 的交互逻辑
9 | ///
10 | public partial class PageClient : Page
11 | {
12 | public PageClient()
13 | {
14 | InitializeComponent();
15 | Loaded += _Loaded;
16 | }
17 |
18 | private void _Loaded(object sender, RoutedEventArgs e)
19 | {
20 | PageManager.SetProfilePage(this, uiListbox, ProfileModule.ClientList);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/code/Messenger/PageFrame.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using Mikodev.Network;
4 | using System;
5 | using System.Windows;
6 | using System.Windows.Controls;
7 |
8 | namespace Messenger
9 | {
10 | ///
11 | /// ProfileFrame.xaml 的交互逻辑
12 | ///
13 | public partial class PageFrame : Page
14 | {
15 | private readonly PageProfile _profPage = new PageProfile();
16 |
17 | public PageFrame()
18 | {
19 | InitializeComponent();
20 | Loaded += _Loaded;
21 | Unloaded += _Unloaded;
22 | }
23 |
24 | private void _Loaded(object sender, RoutedEventArgs e)
25 | {
26 | _profPage.uiLeftFrame.Content = new PageClient();
27 | uiFrame.Content = _profPage;
28 |
29 | var act = (Action)delegate
30 | {
31 | uiSwitchRadio.IsChecked = false;
32 | uiMainBorder.Visibility = Visibility.Collapsed;
33 | };
34 | uiMainBorder.MouseDown += (s, arg) => act.Invoke();
35 | uiMainBorder.TouchDown += (s, arg) => act.Invoke();
36 | HistoryModule.Receive += _HistoryReceiving;
37 | }
38 |
39 | private void _Unloaded(object sender, RoutedEventArgs e)
40 | {
41 | HistoryModule.Receive -= _HistoryReceiving;
42 | }
43 |
44 | ///
45 | /// 如果 Frame 不为用户列表 则消息提示应当存在
46 | ///
47 | private void _HistoryReceiving(object sender, LinkEventArgs e)
48 | {
49 | if (uiFrame.Content == _profPage)
50 | return;
51 | e.Cancel = true;
52 | }
53 |
54 | private void _Click(object sender, RoutedEventArgs e)
55 | {
56 | var tag = (e.OriginalSource as RadioButton)?.Tag as string;
57 | if (tag == null)
58 | return;
59 |
60 | var cur = uiFrame;
61 | var ctx = default(Page);
62 | if (tag == "self")
63 | ctx = new Shower();
64 | else if (tag == "share")
65 | ctx = new PageShare();
66 | else if (tag == "setting")
67 | ctx = new PageOption();
68 | else if (tag != "switch" && cur.Content != _profPage)
69 | ctx = _profPage;
70 |
71 | if (ctx != null)
72 | cur.Content = ctx;
73 |
74 | // Context 属性会延迟生效, 因此只能与 ctx 比较
75 | if (ctx == _profPage)
76 | {
77 | var sco = ProfileModule.Inscope;
78 | if (sco != null)
79 | sco.Hint = 0;
80 | _profPage.uiSearchBox.Text = null;
81 | }
82 |
83 | var lef = _profPage.uiLeftFrame;
84 | if (tag == "user")
85 | lef.Content = new PageClient();
86 | else if (tag == "group")
87 | lef.Content = new PageGroups();
88 | else if (tag == "recent")
89 | lef.Content = new PageRecent();
90 |
91 | if (uiNavigateGrid.Width > uiNavigateGrid.MinWidth)
92 | uiSwitchRadio.IsChecked = false;
93 |
94 | // 清空导航历史
95 | while (NavigationService.CanGoBack)
96 | _ = NavigationService.RemoveBackEntry();
97 |
98 | uiMainBorder.Visibility = uiSwitchRadio.IsChecked == true ? Visibility.Visible : Visibility.Hidden;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/code/Messenger/PageGroups.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/code/Messenger/PageGroups.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using Mikodev.Network;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 |
6 | namespace Messenger
7 | {
8 | ///
9 | /// PageGroups.xaml 的交互逻辑
10 | ///
11 | public partial class PageGroups : Page
12 | {
13 | public PageGroups()
14 | {
15 | InitializeComponent();
16 | Loaded += _Loaded;
17 | }
18 |
19 | private void _Loaded(object sender, RoutedEventArgs e)
20 | {
21 | PageManager.SetProfilePage(this, uiListbox, ProfileModule.GroupList);
22 | }
23 |
24 | private void _Click(object sender, RoutedEventArgs e)
25 | {
26 | var tag = (e.OriginalSource as Button)?.Tag as string;
27 | if (tag == null)
28 | return;
29 | if (tag == "edit")
30 | {
31 | var vis = uiEditGrid.Visibility;
32 | uiEditGrid.Visibility = (vis == Visibility.Visible) ? Visibility.Collapsed : Visibility.Visible;
33 | }
34 | else if (tag == "apply")
35 | {
36 | if (string.Equals(uiEditBox.Text, ProfileModule.GroupLabels) == false && ProfileModule.SetGroupLabels(uiEditBox.Text) == false)
37 | Entrance.ShowError($"最多允许 {Links.GroupLabelLimit} 个群组标签", null);
38 | else
39 | uiEditGrid.Visibility = Visibility.Collapsed;
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/code/Messenger/PageManager.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using System.ComponentModel;
4 | using System.Windows.Controls;
5 |
6 | namespace Messenger
7 | {
8 | internal static class PageManager
9 | {
10 | public static void _ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
11 | {
12 | var lst = e.AddedItems;
13 | if (lst.Count < 1)
14 | return;
15 | var itm = lst[0] as Profile;
16 | ProfileModule.SetInscope(itm);
17 | }
18 |
19 | public static void SetSelectedProfile(ListBox listbox, BindingList list)
20 | {
21 | var idx = list.IndexOf(ProfileModule.Inscope);
22 | listbox.SelectedIndex = idx;
23 | }
24 |
25 | public static void SetProfilePage(Page page, ListBox listbox, BindingList list)
26 | {
27 | SetSelectedProfile(listbox, list);
28 |
29 | var hdr = new ListChangedEventHandler((s, e) =>
30 | {
31 | if (e.ListChangedType != ListChangedType.ItemAdded)
32 | return;
33 | SetSelectedProfile(listbox, list);
34 | });
35 |
36 | list.ListChanged += hdr;
37 | listbox.SelectionChanged += _ListBoxSelectionChanged;
38 |
39 | page.Unloaded += delegate
40 | {
41 | list.ListChanged -= hdr;
42 | listbox.SelectionChanged -= _ListBoxSelectionChanged;
43 | };
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/code/Messenger/PageOption.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | 设置
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/code/Messenger/PageOption.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger
6 | {
7 | ///
8 | /// PageOption.xaml 的交互逻辑
9 | ///
10 | public partial class PageOption : Page
11 | {
12 | public PageOption()
13 | {
14 | InitializeComponent();
15 | }
16 |
17 | private void _Click(object sender, RoutedEventArgs e)
18 | {
19 | var tag = (e.OriginalSource as Button)?.Tag as string;
20 | if (tag == null)
21 | return;
22 | if (tag == "exit")
23 | {
24 | LinkModule.Shutdown();
25 | Application.Current.MainWindow.Close();
26 | }
27 | else if (tag == "out")
28 | {
29 | var mai = Application.Current.MainWindow as Entrance;
30 | if (mai == null)
31 | return;
32 | LinkModule.Shutdown();
33 | ProfileModule.Shutdown();
34 | _ = mai.frame.Navigate(new Connection());
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/code/Messenger/PageProfile.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/code/Messenger/PageProfile.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Models;
2 | using Messenger.Modules;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 |
9 | namespace Messenger
10 | {
11 | ///
12 | /// ProfilePage.xaml 的交互逻辑
13 | ///
14 | public partial class PageProfile : Page
15 | {
16 | public PageProfile()
17 | {
18 | InitializeComponent();
19 | Loaded += _Loaded;
20 | Unloaded += _Unloaded;
21 | }
22 |
23 | private void _Loaded(object sender, RoutedEventArgs e)
24 | {
25 | ProfileModule.InscopeChanged += ModuleProfile_InscopeChanged;
26 | uiProfileList.SelectionChanged += PageManager._ListBoxSelectionChanged;
27 | }
28 |
29 | private void _Unloaded(object sender, RoutedEventArgs e)
30 | {
31 | ProfileModule.InscopeChanged -= ModuleProfile_InscopeChanged;
32 | uiProfileList.SelectionChanged -= PageManager._ListBoxSelectionChanged;
33 | }
34 |
35 | private void ModuleProfile_InscopeChanged(object sender, EventArgs e)
36 | {
37 | var pag = uiRightFrame.Content as Chatter;
38 | if (pag == null || pag.Profile.Id != ProfileModule.Inscope.Id)
39 | _ = uiRightFrame.Navigate(new Chatter());
40 | }
41 |
42 | ///
43 | /// 根据用户昵称和签名提供搜索功能
44 | ///
45 | private void _TextChanged(object sender, TextChangedEventArgs e)
46 | {
47 | if (e.OriginalSource != uiSearchBox)
48 | return;
49 | var lst = uiProfileList.ItemsSource as ICollection;
50 | lst?.Clear();
51 | if (string.IsNullOrWhiteSpace(uiSearchBox.Text) == true)
52 | {
53 | uiProfileList.ItemsSource = null;
54 | uiPanel.Visibility = Visibility.Collapsed;
55 | }
56 | else
57 | {
58 | var txt = uiSearchBox.Text.ToLower();
59 | var val = (from i in ProfileModule.ClientList.Union(ProfileModule.GroupList).Union(ProfileModule.RecentList)
60 | where i.Name?.ToLower().Contains(txt) == true || i.Text?.ToLower().Contains(txt) == true
61 | select i).ToList();
62 | var idx = val.IndexOf(ProfileModule.Inscope);
63 | uiProfileList.ItemsSource = val;
64 | uiProfileList.SelectedIndex = idx;
65 | uiPanel.Visibility = Visibility.Visible;
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/code/Messenger/PageRecent.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 最近
23 | 所有最近的用户
24 |
25 |
29 |
30 |
31 |
32 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/code/Messenger/PageRecent.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger
6 | {
7 | ///
8 | /// PageRecent.xaml 的交互逻辑
9 | ///
10 | public partial class PageRecent : Page
11 | {
12 | public PageRecent()
13 | {
14 | InitializeComponent();
15 | Loaded += _Loaded;
16 | }
17 |
18 | private void _Loaded(object sender, RoutedEventArgs e)
19 | {
20 | PageManager.SetProfilePage(this, uiListbox, ProfileModule.RecentList);
21 | }
22 |
23 | private void _Click(object sender, RoutedEventArgs e)
24 | {
25 | var tag = (e.OriginalSource as Button)?.Tag as string;
26 | if (tag == null)
27 | return;
28 | if (tag == "clear")
29 | {
30 | ProfileModule.RecentList.Clear();
31 | return;
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/code/Messenger/PageShare.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
44 |
45 |
46 |
47 |
50 |
51 |
53 |
56 |
57 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/code/Messenger/PageShare.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using Mikodev.Logger;
3 | using System;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 |
10 | namespace Messenger
11 | {
12 | ///
13 | /// Interaction logic for Transform.xaml
14 | ///
15 | public partial class PageShare : Page
16 | {
17 | public PageShare()
18 | {
19 | InitializeComponent();
20 | }
21 |
22 | private void _Click(object sender, RoutedEventArgs e)
23 | {
24 | var tag = (e.OriginalSource as Button)?.Tag as string;
25 | if (tag == null)
26 | return;
27 |
28 | if (tag == "clean")
29 | {
30 | _ = ShareModule.Remove();
31 | }
32 | else if (tag == "change")
33 | {
34 | var dfd = new System.Windows.Forms.FolderBrowserDialog();
35 | if (Directory.Exists(ShareModule.SavePath))
36 | dfd.SelectedPath = ShareModule.SavePath;
37 | if (dfd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
38 | ShareModule.SavePath = dfd.SelectedPath;
39 | }
40 | else if (tag == "open")
41 | {
42 | _ = Task.Run(() =>
43 | {
44 | try
45 | {
46 | if (Directory.Exists(ShareModule.SavePath) == false)
47 | return;
48 | using (Process.Start("explorer", "/e," + ShareModule.SavePath)) { }
49 | }
50 | catch (Exception ex)
51 | {
52 | Log.Error(ex);
53 | }
54 | });
55 | }
56 | else if (tag == "stop")
57 | {
58 | ShareModule.Shutdown();
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/code/Messenger/Resources/Container.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
56 |
57 |
--------------------------------------------------------------------------------
/code/Messenger/Resources/Geometry.xaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
13 |
14 |
15 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/code/Messenger/Resources/Validation.xaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
70 |
71 |
--------------------------------------------------------------------------------
/code/Messenger/Shower.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 昵称
38 |
39 | 个人签名
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/code/Messenger/Shower.xaml.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Modules;
2 | using Mikodev.Logger;
3 | using System;
4 | using System.Windows;
5 | using System.Windows.Controls;
6 |
7 | namespace Messenger
8 | {
9 | ///
10 | /// Interaction logic for Shower.xaml
11 | ///
12 | public partial class Shower : Page
13 | {
14 | public Shower()
15 | {
16 | InitializeComponent();
17 | }
18 |
19 | private void _Click(object sender, RoutedEventArgs e)
20 | {
21 | var tag = (e.OriginalSource as Button)?.Tag as string;
22 | if (tag == null)
23 | return;
24 | if (tag == "apply")
25 | {
26 | ProfileModule.SetProfile(uiNameBox.Text, uiSignBox.Text);
27 | }
28 | else if (tag == "image")
29 | {
30 | var ofd = new System.Windows.Forms.OpenFileDialog() { Filter = "位图文件|*.bmp;*.png;*.jpg" };
31 | if (ofd.ShowDialog() != System.Windows.Forms.DialogResult.OK)
32 | return;
33 | try
34 | {
35 | ProfileModule.SetImage(ofd.FileName);
36 | }
37 | catch (Exception ex)
38 | {
39 | Entrance.ShowError("设置头像失败!", ex);
40 | Log.Error(ex);
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/ImageSourceConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 | using System.Windows.Media;
5 | using System.Windows.Media.Imaging;
6 |
7 | namespace Messenger.Tools
8 | {
9 | internal class ImageSourceConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | var str = value as string;
14 | if (string.IsNullOrEmpty(str))
15 | return null;
16 | return new ImageBrush(new BitmapImage(new Uri(str)));
17 | }
18 |
19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/LengthUnitConverter.cs:
--------------------------------------------------------------------------------
1 | using Messenger.Extensions;
2 | using System;
3 | using System.Globalization;
4 | using System.Windows.Data;
5 |
6 | namespace Messenger.Tools
7 | {
8 | ///
9 | /// 将大小转化为带单位的字符串
10 | ///
11 | internal class LengthUnitConverter : IValueConverter
12 | {
13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => Extension.ToUnitEx(System.Convert.ToInt64(value));
14 |
15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/LogicToPixelConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows;
4 | using System.Windows.Data;
5 | using System.Windows.Media;
6 |
7 | namespace Messenger.Tools
8 | {
9 | internal class LogicToPixelConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | // 将像素尺寸转换到逻辑尺寸 (用于在高 DPI 环境以像素为单位进行操作)
14 | if (value is Visual vis && targetType == typeof(Thickness) && parameter is Thickness mar)
15 | {
16 | var win = PresentationSource.FromVisual(vis);
17 | var hor = 1.0 / win.CompositionTarget.TransformToDevice.M11;
18 | var ver = 1.0 / win.CompositionTarget.TransformToDevice.M22;
19 | var thi = new Thickness(hor * mar.Left, ver * mar.Top, hor * mar.Right, ver * mar.Bottom);
20 | return thi;
21 | }
22 | return null;
23 | }
24 |
25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/ObjectTypeConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace Messenger.Tools
6 | {
7 | internal class ObjectTypeConverter : IValueConverter
8 | {
9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value?.GetType();
10 |
11 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/ProfileHintConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace Messenger.Tools
6 | {
7 | ///
8 | /// 当未读消息过多时 标注 "+" 号
9 | ///
10 | internal class ProfileHintConverter : IValueConverter
11 | {
12 | public int MaxShowValue { get; set; } = 9;
13 |
14 | public string OverflowText { get; set; } = "9+";
15 |
16 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
17 | {
18 | switch (value)
19 | {
20 | case int val when val > MaxShowValue:
21 | return OverflowText;
22 |
23 | case int val when val < 0 == false:
24 | return val.ToString();
25 |
26 | default:
27 | return 0.ToString();
28 | }
29 | }
30 |
31 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/ProfileIdValidation.cs:
--------------------------------------------------------------------------------
1 | using Mikodev.Network;
2 | using System.Globalization;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger.Tools
6 | {
7 | internal class ProfileIdValidation : ValidationRule
8 | {
9 | public override ValidationResult Validate(object value, CultureInfo cultureInfo)
10 | {
11 | var str = value as string;
12 | if (string.IsNullOrEmpty(str))
13 | return new ValidationResult(false, "输入为空");
14 | if (int.TryParse(str, out var id))
15 | if (Links.Id < id && id < Links.DefaultId)
16 | return new ValidationResult(true, string.Empty);
17 | else
18 | return new ValidationResult(false, $"编号应大于 {Links.Id} 且小于 {Links.DefaultId}");
19 | return new ValidationResult(false, "输入无效");
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/ProfileLogoConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Text.RegularExpressions;
4 | using System.Windows.Data;
5 |
6 | namespace Messenger.Tools
7 | {
8 | ///
9 | /// 为没有头像的用户生成字符 Logo
10 | ///
11 | internal class ProfileLogoConverter : IValueConverter
12 | {
13 | private const int _limit = 3;
14 |
15 | private const int _short = 2;
16 |
17 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
18 | {
19 | var reg = new Regex(@"^[A-Za-z0-9]+$");
20 | var str = (value == null) ? string.Empty : value.ToString();
21 | if (str.Length > _limit && _limit > _short && _short > 0)
22 | str = str.Substring(0, _short);
23 | if (str.Length > 1 && reg.IsMatch(str) == false)
24 | str = str.Substring(0, 1);
25 | return str.ToUpper();
26 | }
27 |
28 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/SocketPortValidation.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Net;
3 | using System.Windows.Controls;
4 |
5 | namespace Messenger.Tools
6 | {
7 | internal class SocketPortValidation : ValidationRule
8 | {
9 | public override ValidationResult Validate(object value, CultureInfo cultureInfo)
10 | {
11 | var str = value as string;
12 | if (string.IsNullOrEmpty(str))
13 | return new ValidationResult(false, "输入为空");
14 | if (int.TryParse(str, out var val))
15 | if (val >= IPEndPoint.MinPort && val <= IPEndPoint.MaxPort)
16 | return new ValidationResult(true, string.Empty);
17 | else
18 | return new ValidationResult(false, $"端口号应当介于 {IPEndPoint.MinPort} 和 {IPEndPoint.MaxPort} 之间");
19 | return new ValidationResult(false, "输入无效");
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/code/Messenger/Tools/StringEmptyConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace Messenger.Tools
6 | {
7 | internal class StringEmptyConverter : IValueConverter
8 | {
9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => string.IsNullOrEmpty(value?.ToString());
10 |
11 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/image/00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/00.png
--------------------------------------------------------------------------------
/image/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/01.png
--------------------------------------------------------------------------------
/image/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/02.png
--------------------------------------------------------------------------------
/image/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/03.png
--------------------------------------------------------------------------------